---
title: EMC Pre-Compliance Testing on Your Bench
description: "Run EMC pre-compliance testing on your bench: near-field probing, LISN conducted scans, radiated pre-scans, before/after comparisons and automated sweeps."
url: https://galoislabs.ai/blog/emc-pre-compliance-testing
author: Alex Hernandez
author_url: https://galoislabs.ai/blog/authors/alex-hernandez
published: "2026-08-22"
topic: Industries
publisher: Galois Labs
---

# EMC pre-compliance testing on your bench: probes, LISN scans and repeatable sweeps

![An EMC pre-compliance setup in side elevation: a near-field loop probe on a stand arm hovering over a board, and a LISN box beside it.](https://galoislabs.ai/blog/figures/lab-2.light.webp)

*FIG. 1 — NEAR-FIELD PROBE OVER THE BOARD*

EMC pre-compliance testing is measuring your product's emissions on your own bench before an accredited lab does: near-field probes to find sources, a LISN and spectrum analyzer for conducted emissions, and an antenna for a radiated pre-scan. It does not certify anything. It tells you your margins while a fix is still a layout change, not a lab retest.

First attempts fail often: Intertek reports EMC failure rates around 50% on first tests in its labs, and 5 to 7% on retests (Source: [Intertek white paper](https://web.archive.org/web/20220424064926/https://www.intertek.com/uploadedFiles/Intertek/Divisions/Commercial_and_Electrical/Media/PDF/EMC_Testing/Why-50-Percent-Fail-EMC-WP.pdf)). Lab time is the expensive part of the loop: Tektronix, which sells pre-compliance equipment, puts an external test lab at $1,000 to $10,000 a day (Source: [Tektronix](https://www.tek.com/en/blog/financial-case-emi-emc-pre-compliance-test-solution)). A bench scan costs no lab time, so you can repeat it after every layout change, ferrite and firmware build, as long as every scan is taken the same way. This guide covers the measurements, then the comparisons and automation that make them repeatable, with code for a Rigol DSA815 and FCC Part 15 Class B limits.

## What is EMC pre-compliance testing?

Formal emissions testing follows the full method of your product's standard: a measuring receiver, a characterized site and a defined setup. In the US, FCC Part 15 sets the limits for digital devices, and [47 CFR 15.35](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-A/section-15.35) bases the limits up to 1 GHz on a CISPR quasi-peak detector and its related measurement bandwidths. Pre-compliance repeats those measurements with bench equipment, trading accuracy for the freedom to run them whenever you want.

Three measurements do most of the work:

- **Near-field probing** locates sources: which loop, net, heatsink or cable. It has no limit line, so every result is relative.
- **Conducted emissions** measure the noise a product pushes back onto its AC power line, 150 kHz to 30 MHz, through a LISN. The LISN fixes the impedance the product sees, which makes this the most repeatable of the three.
- **Radiated pre-scans** measure field strength from 30 MHz up with an antenna. Without a characterized site, treat the absolute numbers with suspicion and the trends as real.

### Which emission limits apply?

Take the limits from your product's standard and market. For a [Class B digital device](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-A/section-15.3), one marketed for use in a residential environment, FCC Part 15 sets these limits in [15.107(a)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-B/section-15.107) and [15.109(a)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-B/section-15.109). The lower limit applies at each band edge, and dBµV/m is 20 log10 of the field strength in µV/m.

| Measurement        | Frequency (MHz) | Quasi-peak limit                  | Average limit                     |
| ------------------ | --------------- | --------------------------------- | --------------------------------- |
| Conducted, AC line | 0.15 to 0.5     | 66 to 56 dBµV, falling with log f | 56 to 46 dBµV, falling with log f |
| Conducted, AC line | 0.5 to 5        | 56 dBµV                           | 46 dBµV                           |
| Conducted, AC line | 5 to 30         | 60 dBµV                           | 50 dBµV                           |
| Radiated at 3 m    | 30 to 88        | 40.0 dBµV/m (100 µV/m)            | —                                 |
| Radiated at 3 m    | 88 to 216       | 43.5 dBµV/m (150 µV/m)            | —                                 |
| Radiated at 3 m    | 216 to 960      | 46.0 dBµV/m (200 µV/m)            | —                                 |
| Radiated at 3 m    | 960 to 1,000    | 54.0 dBµV/m (500 µV/m)            | —                                 |

Class A devices, marketed for commercial, industrial or business use, get higher conducted limits (79 dBµV quasi-peak below 0.5 MHz, 73 dBµV above) and radiated limits specified at 10 m. Above 1 GHz, [15.35(b)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-A/section-15.35) switches to an average detector with at least 1 MHz resolution bandwidth, plus a peak limit 20 dB above the average limit.

## What equipment do you need for EMC pre-compliance testing?

| Tool                                  | What it finds                       | Setting that matters                                                | Read results as                                   |
| ------------------------------------- | ----------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------- |
| H-field and E-field near-field probes | The loop, net or part that radiates | Fixed position and orientation                                      | Relative dB, before against after                 |
| LISN, 50 µH/50 Ω                      | Noise conducted onto the AC line    | Bonded to a ground plane; idle port terminated in 50 Ω              | dBµV against the conducted limit                  |
| Transient limiter                     | Nothing; it protects the analyzer   | Its loss entered as a correction                                    | —                                                 |
| Spectrum analyzer with EMI bandwidths | All three measurements              | 9 kHz or 120 kHz bandwidth; peak first, then quasi-peak and average | dBµV, or dBµV/m with antenna factors              |
| Broadband antenna and cable           | Radiated emissions, 30 MHz to 1 GHz | Antenna factor and cable loss as corrections                        | dBµV/m; relative unless the site is characterized |
| RF current probe                      | Common-mode current on cables       | Same clamp position on each cable                                   | Relative dB                                       |

The code in this guide drives a Rigol DSA815 with the EMI-DSA800 option, which Rigol describes as an "EMI filter (6 dB) and quasi-peak detectors" ([DSA800 product page](https://www.rigolna.com/products/spectrum-analyzers/dsa800/)). With the EMI filter or the quasi-peak detector on, the [DSA800 programming guide](https://beyondmeasure.rigoltech.com/acton/attachment/1579/f-06f0/1/-/-/-/-/dsa800programpdf.pdf) allows only three resolution bandwidths: 200 Hz, 9 kHz and 120 kHz. Those are the CISPR measurement bandwidths; use 9 kHz for the 150 kHz to 30 MHz conducted band and 120 kHz from 30 MHz to 1 GHz.

Rigol rates the RF input at 50 V DC and +20 dBm continuous ([DSA800 user guide](https://beyondmeasure.rigoltech.com/acton/attachment/1579/f-9be15540-c791-4857-8168-b2378207dc5c/1/-/-/-/-/DSA800%28E%29_UserGuide_EN.pdf)). A LISN port can deliver transients beyond that when the product switches on, so the limiter is not optional.

## How do you find EMI sources with near-field probes?

Near-field probes answer where, not how much. An H-field loop responds to magnetic field, so it finds current: switching loops, clock lines, broken return paths. An E-field probe responds to voltage, so it finds fast voltage edges: switch nodes, heatsinks, unterminated traces. Start with a large loop to find the region, then a small one to find the part.

Set the analyzer to positive peak with max hold, center it on a frequency your scan flagged, and move the probe slowly. A loop picks up only the flux passing through it, so rotate it at each spot and note the orientation of the maximum.

Probe readings do not convert to the field strength a lab measures at 3 m. A 10 dB drop at the probe means the source got quieter; the radiated margin can move by more or less. Use the probe to choose fixes and the scans to judge them.

Repeatability is the whole game. Hold the probe in a fixture or mark each position on a photo of the board, record probe, orientation and height, and run the same mode and firmware build every time. Otherwise a few millimeters of probe movement can outweigh the fix.

For radiated problems at the low end of the band, check cables too. Clamp a current probe around each cable at the same position and look for common-mode current at the flagged frequencies; cables are often the antenna.

## How do you measure conducted emissions with a LISN and spectrum analyzer?

### Set up the LISN and protect the analyzer

[47 CFR 15.107](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-B/section-15.107) measures conducted emissions through a 50 µH/50 Ω LISN, as the radio frequency voltage "between each power line and ground at the power terminal." Scan line and neutral separately and keep both records.

1. Bond the LISN to a ground plane and earth it before applying mains. LISNs pass leakage current to ground by design; follow the manual's grounding instructions.
2. Terminate the measurement port you are not using in 50 Ω.
3. Connect the transient limiter to the port you are measuring, then the analyzer.
4. Keep the product, cables and LISN in the same positions for every scan, and photograph the setup.
5. Put the limiter's loss and the LISN's voltage division factor, from their calibration data, in correction files so readings refer to the LISN terminals.

The DSA800 can apply corrections itself, up to 200 points per set. The script applies them in Python instead, so each record holds the raw trace next to the corrected one.

Under [15.107(d)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-B/section-15.107), a battery-only product that cannot operate from the AC line needs no conducted measurement. One that operates while charging, through an AC adapter, or on power from another device plugged into the AC line, does.

### Scan with peak first, then quasi-peak where it matters

The quasi-peak detector is slow by design. [Rigol describes it](https://beyondmeasure.rigoltech.com/acton/attachment/1579/f-9be15540-c791-4857-8168-b2378207dc5c/1/-/-/-/-/DSA800%28E%29_UserGuide_EN.pdf) as "a weighted form of peak detector" whose charge time is much shorter than its discharge time, so each point needs time to settle. [15.35(a)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-A/section-15.35) lets you demonstrate compliance with a peak detector at the CISPR bandwidths, and a quasi-peak reading never exceeds the peak reading of the same signal. So sweep the whole band with the positive-peak detector, keep the maximum over several sweeps, and re-measure with quasi-peak only the peaks within your margin of the limit. Conducted emissions also have an average limit 10 dB lower, so the script screens against it and re-measures conducted suspects with the voltage-average detector too.

Two DSA800 details from the programming guide shape the code. `:INIT` starts a sweep only in single-sweep mode, and `*OPC?` returns 0 while an operation is still running rather than blocking, so the script polls it. With the positive-peak detector, each trace point shows the maximum sampled in its interval, so a narrow emission between points keeps its amplitude. Only its frequency is approximate, and the narrow-span quasi-peak measurement pins it down.

First, the limits as code:

```python title="emc/limits.py"
"""FCC Part 15 Class B limits: conducted per 47 CFR 15.107(a), radiated at 3 m per 15.109(a)."""
import numpy as np


def conducted_class_b(f_hz: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Quasi-peak and average limits in dBuV, 150 kHz to 30 MHz."""
    f = np.asarray(f_hz) / 1e6
    qp = np.where(
        f <= 0.5,
        66 - 10 * np.log10(f / 0.15) / np.log10(0.5 / 0.15),  # 66 to 56, linear in log f
        np.where(f <= 5, 56.0, 60.0),                          # lower limit at the 5 MHz edge
    )
    return qp, qp - 10.0  # the average limit sits 10 dB below in every segment


def radiated_class_b_3m(f_hz: np.ndarray) -> tuple[np.ndarray, None]:
    """Quasi-peak field-strength limit in dBuV/m up to 1 GHz; tighter limit at band edges."""
    f = np.asarray(f_hz) / 1e6
    uv_per_m = np.select([f <= 88, f <= 216, f <= 960], [100.0, 150.0, 200.0], 500.0)
    return 20 * np.log10(uv_per_m), None


LIMITS = {"conducted": conducted_class_b, "radiated": radiated_class_b_3m}
```

Then the scan, using PyVISA and the commands from Rigol's programming guide:

```python title="emc/scan.py"
"""Peak pre-scan with max hold, then quasi-peak on the suspects (Rigol DSA815 + EMI-DSA800)."""
import json
import sys
import time
from datetime import datetime, timezone

import numpy as np
import pyvisa

from emc.limits import LIMITS

DBM_TO_DBUV = 107.0  # 0 dBm across 50 ohms is 107 dBuV
BANDS = {
    "conducted": {"start": 150e3, "stop": 30e6, "rbw": 9e3},
    "radiated": {"start": 30e6, "stop": 1e9, "rbw": 120e3},
}
POINTS, SWEEPS, SUSPECTS = 3001, 10, 10
MARGIN_DB = 6.0  # placeholder: set it from your own lab correlation


def configure(sa, start: float, stop: float, rbw: float, detector: str, points: int) -> None:
    for cmd in (
        "*CLS",
        ":INIT:CONT OFF",              # single sweeps: :INIT is ignored in continuous mode
        f":SENS:FREQ:STAR {start:.0f}",
        f":SENS:FREQ:STOP {stop:.0f}",
        ":SENS:BAND:EMIF:STAT ON",     # 6 dB EMI filter
        f":SENS:BAND:RES {rbw:.0f}",   # 200 Hz, 9 kHz or 120 kHz with the filter on
        f":SENS:DET:FUNC {detector}",  # POS for the pre-scan, QPE and VAV for suspects
        f":SENS:SWE:POIN {points}",    # 101 to 3001
        ":TRAC1:MODE WRIT",            # max hold happens in Python, one sweep at a time
        ":UNIT:POW DBM",
        ":FORM:TRAC:DATA ASC",
    ):
        sa.write(cmd)
    if not (err := sa.query(":SYST:ERR?")).startswith(("0,", "+0,")):
        raise RuntimeError(f"analyzer rejected the setup: {err}")


def single_sweep(sa, timeout_s: float = 300.0) -> np.ndarray:
    sa.write(":INIT")
    deadline = time.monotonic() + timeout_s
    while sa.query("*OPC?").strip() != "1":  # the DSA800 answers 0 while busy
        if time.monotonic() > deadline:
            raise TimeoutError("sweep did not finish")
        time.sleep(0.2)
    raw = sa.query(":TRAC:DATA? TRACE1")
    if raw.startswith("#"):                  # ASCII data still arrives behind a #9 header
        raw = raw[2 + int(raw[1]):]
    return np.array([float(v) for v in raw.split(",") if v.strip()]) + DBM_TO_DBUV


def correction_db(freqs: np.ndarray, paths: list[str]) -> np.ndarray:
    """dB to add, interpolated in log frequency: limiter and LISN, or antenna and cable."""
    total = np.zeros(len(freqs))
    for path in paths:
        f, db = np.loadtxt(path, delimiter=",", unpack=True)  # rows of freq_hz,dB
        total += np.interp(np.log10(freqs), np.log10(f), db)
    return total


def local_peaks(y: np.ndarray, threshold: np.ndarray) -> list[int]:
    idx = [i for i in range(1, len(y) - 1)
           if y[i] > threshold[i] and y[i] >= y[i - 1] and y[i] >= y[i + 1]]
    return sorted(idx, key=lambda i: y[i] - threshold[i], reverse=True)


def remeasure(sa, lo: float, span: float, rbw: float, detector: str, factors: list[str]) -> tuple[float, float]:
    """Narrow-span sweep with a slow detector; returns the frequency and corrected level of its maximum."""
    configure(sa, lo, lo + span, rbw, detector, 101)
    trace = single_sweep(sa, timeout_s=900)
    j = int(np.argmax(trace))
    f = lo + j * span / (len(trace) - 1)
    return f, float(trace[j] + correction_db(np.array([f]), factors)[0])


def scan(resource: str, band: str, label: str, dut: str, factors: list[str]) -> dict:
    b, limits = BANDS[band], LIMITS[band]
    rm = pyvisa.ResourceManager()
    sa = rm.open_resource(resource, read_termination="\n", write_termination="\n", timeout=60_000)
    idn = sa.query("*IDN?").strip()

    configure(sa, b["start"], b["stop"], b["rbw"], "POS", POINTS)
    raw = np.max([single_sweep(sa) for _ in range(SWEEPS)], axis=0)  # max hold
    freqs = np.linspace(b["start"], b["stop"], len(raw))
    peak = raw + correction_db(freqs, factors)
    qp_limit, avg_limit = limits(freqs)
    screen = qp_limit if avg_limit is None else avg_limit  # screen against the lowest limit

    suspects = []
    for i in local_peaks(peak, screen - MARGIN_DB)[:SUSPECTS]:
        span = 25 * b["rbw"]
        lo = float(np.clip(freqs[i] - span / 2, b["start"], b["stop"] - span))  # stay in band
        f_qp, qp = remeasure(sa, lo, span, b["rbw"], "QPE", factors)
        s = {"freq_hz": f_qp, "peak": float(peak[i]), "qp": qp,
             "qp_margin_db": float(limits(np.array([f_qp]))[0][0] - qp)}
        if avg_limit is not None:  # conducted: average limit as well
            f_av, av = remeasure(sa, lo, span, b["rbw"], "VAV", factors)
            s |= {"avg": av, "avg_margin_db": float(limits(np.array([f_av]))[1][0] - av)}
        suspects.append(s)
    sa.close()

    return {
        "label": label, "dut": dut, "band": band, "idn": idn,
        "time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "settings": {**b, "points": len(raw), "sweeps": SWEEPS, "detector": "POS",
                     "margin_db": MARGIN_DB, "factors": factors},
        "freq_hz": freqs.round().tolist(),
        "raw_dbuv": raw.round(2).tolist(),
        "peak": peak.round(2).tolist(),
        "worst_peak_margin_db": float(np.min(screen - peak)),
        "suspects": suspects,
    }


if __name__ == "__main__":
    # python -m emc.scan conducted baseline SN0042 TCPIP0::192.168.1.60::5555::SOCKET lisn_limiter.csv
    band, label, dut, resource, *factors = sys.argv[1:]
    record = scan(resource, band, label, dut, factors)
    out = f"{dut}_{band}_{label}_{record['time'][:19].replace(':', '')}.json"
    with open(out, "w") as f:
        json.dump(record, f)
    print(out, f"worst peak margin {record['worst_peak_margin_db']:+.1f} dB,",
          f"{len(record['suspects'])} suspects re-measured")
```

Port 5555 is the socket port Rigol's programming guide gives for the DSA800. Max hold runs in Python, one sweep at a time, so the result never depends on what an earlier session left in trace memory. Raise `SWEEPS` if your product has slow modes.

Run the script once with the product powered off but in place, labeled `ambient`. Any peak within a few dB of the ambient trace is the room, not the product.

Choose `MARGIN_DB` before you scan and record it with the results. The right value depends on how closely your setup tracks the lab's; to learn that, scan a unit the lab has already measured and compare at the same frequencies.

## How do you run a radiated emissions pre-scan?

The same script runs the radiated band, 30 MHz to 1 GHz at 120 kHz, with antenna factor and cable loss as the correction files. The antenna factor converts antenna-port voltage to field strength, so the corrected trace reads in dBµV/m.

Distance needs care. [15.31(f)(1)](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-A/part-15/subpart-A/section-15.31) allows measurements at a distance other than the one specified at 30 MHz and above, extrapolated at 20 dB per decade, but generally not in the near field. At 30 MHz the wavelength is 10 m, so a 1 m bench setup sits in the near field at the low end of the band. Read 1 m results as relative. To compare a 3 m measurement with Class A limits written for 10 m, the extrapolation is 20 log10(10/3), about 10.5 dB.

Outside a shielded room, ambient signals appear on every scan; the [FM broadcast band](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-C/part-73/subpart-B/section-73.201) alone covers 88 to 108 MHz. Compare against the ambient scan before chasing anything.

A lab rotates the product and moves the antenna to find each emission's maximum. On the bench, rotate the product through fixed steps, scan both antenna polarizations, keep the maximum, and record which orientation produced it. Route cables the same way every time.

## How do you compare EMC scans before and after a fix?

A fix is proven by a comparison: same setup, same settings, one change. The scan script writes every setting into its record, and the comparison refuses to run when they differ.

```python title="emc/compare.py"
"""python -m emc.compare before.json after.json"""
import json
import sys

import numpy as np

from emc.limits import LIMITS


def load(path: str) -> dict:
    with open(path) as f:
        return json.load(f)


before, after = load(sys.argv[1]), load(sys.argv[2])
if (before["band"], before["settings"]) != (after["band"], after["settings"]):
    raise SystemExit("settings differ: re-run both scans with one configuration")

f = np.array(before["freq_hz"])
b, a = np.array(before["peak"]), np.array(after["peak"])
qp, avg = LIMITS[before["band"]](f)
limit = qp if avg is None else avg  # the same screening limit the scan used
unit = "dBuV/m" if before["band"] == "radiated" else "dBuV"
delta = a - b

print(f"{before['label']} -> {after['label']}: worst peak margin "
      f"{np.min(limit - b):+.1f} -> {np.min(limit - a):+.1f} dB")
for title, order in (("most improved", np.argsort(delta)), ("most worsened", np.argsort(-delta))):
    print(title)
    for i in order[:5]:
        print(f"  {f[i] / 1e6:9.3f} MHz  {b[i]:6.1f} -> {a[i]:6.1f} {unit}  "
              f"({delta[i]:+.1f} dB, margin {limit[i] - a[i]:+.1f} dB)")
```

Three habits make the deltas trustworthy:

- **Measure your noise first.** Scan the baseline twice with no change. The spread between the two is the smallest delta you can believe.
- **Change one thing per scan.** A ferrite, a series resistor, a firmware change to edge rates: one at a time, each with its own labeled record.
- **Read the whole sweep.** A fix that lowers one peak can raise another, so check the most-worsened list too.

Keep every record. When the lab report comes back, the scans of that build are what you correlate against.

## How do you automate EMC pre-compliance sweeps and reports?

Scripts fix the settings. They leave open running the same scan on every bench, tying each record to its unit and build, and turning a folder of JSON into something a reviewer can read. That is the layer Galois builds.

Galois is agent-driven test engineering for hardware teams: agents generate tests and instrument drivers, run them on real benches through the open-source galois-edge daemon, and turn the results into reports and a shared engineering record.

For an EMC bench, that means:

- **The analyzer as a known instrument.** Galois ships 573 instrument profiles across 135 manufacturers ([instrument library](https://galoislabs.ai/instruments)), and galois-edge matches a Rigol DSA800's `*IDN?` reply to its profile. If your analyzer has no profile, the [PDF-to-driver pipeline](https://galoislabs.ai/compare/labview) generates one from its programming manual, and you check it against the manual before it runs ([declarative instrument drivers](https://galoislabs.ai/blog/declarative-instrument-drivers)).
- **The same script, from another machine.** With `pyvisa-galois`, `pyvisa.ResourceManager("@galois")` replaces one line, and the scan runs through the daemon on the bench PC ([PyVISA backend docs](https://docs.galoislabs.ai/guides/pyvisa/); [SCPI automation guide](https://galoislabs.ai/blog/scpi-automation-python)).
- **Approved procedures.** As a Galois sequence, a draft scan cannot run until an engineer approves it, and an approved sequence that is edited must be approved again. Each step records the SCPI sent, the raw response, the measured value, its limits and the instrument; each run records the operator and the DUT serial ([product](https://galoislabs.ai/product)).
- **Reports from the record.** Évariste, the agent in the Galois platform, drafts a report from the run records, or a data-bound template fills your house format, for an engineer to review.
- **Agents at the bench.** Through the daemon's MCP tools, an agent can configure the analyzer, run the scan and re-measure suspects, with the same audit log as the gRPC API. [MCP for lab instruments](https://galoislabs.ai/blog/mcp-lab-instruments) covers setup, and the [MCP server reference](https://docs.galoislabs.ai/agents/mcp-server/) the access model.

Évariste can also run these scans from a plain-English objective ([next section](https://galoislabs.ai/blog/emc-pre-compliance-testing#how-to-run-emc-pre-compliance-scans-in-galois-with-évariste)).

A compliance-check branch that assembles standards findings and sign-off packages from run evidence is in build. None of this certifies a product: the records support the formal test and your conformity process, and they do not replace either. Teams that keep test data in-house can run the platform as a dedicated single-tenant cloud or fully on-prem and air-gapped ([deployment options](https://galoislabs.ai/deployment)).

## How to run EMC pre-compliance scans in Galois with Évariste

Here is the same conducted scan run with Évariste through galois-edge; [AI test automation for hardware benches](https://galoislabs.ai/blog/ai-test-automation-hardware) covers the agent in general.

1. **Check the analyzer.** Open Évariste from the app sidebar (Ctrl+Shift+E) and ask it to "List connected instruments"; the DSA815 appears on your edge with its profile. If it has none, upload the programming guide PDF: Évariste generates a profile, and you check it against the manual, EMI-filter bandwidths included, before it is deployed to the edge and bound.

2. **State the objective** with the numbers the script hard-codes:

   > Create a conducted emissions pre-scan for the Rigol DSA815 with the EMI-DSA800 option: 150 kHz to 30 MHz, EMI filter on, 9 kHz RBW, positive-peak detector, 3001 points, maximum of 10 single sweeps, LISN and limiter corrections applied. Screen the peaks against the FCC Part 15 Class B average limit in 15.107(a) with a 6 dB margin, then re-measure up to 10 suspects in a span of 25 times the RBW with the quasi-peak and voltage-average detectors, against their 15.107(a) limits.

3. **Review and approve the draft.** Évariste writes the pre-scan as a draft sequence: the setup from `configure()`, ten single sweeps under the analyzer's max hold, the corrections loaded into its correction sets, and steps recording the trace and its highest peaks. Suspect steps come after the first scan. A draft cannot run. Check bandwidth, detector, points, trace mode, units and corrections against the programming guide and your correction files, then approve it ([reviewing an AI-generated test plan](https://galoislabs.ai/blog/review-ai-generated-test-plan)).

4. **Run it on the bench**: once with the product off for ambient, then powered for the baseline on line and on neutral. galois-edge drives the analyzer while you follow it in Monitor.

5. **Re-measure the suspects.** Ask which peaks come within 6 dB of the average limit, or exceed it, and are absent from the ambient run. Évariste lists them; check them against the recorded trace, then it adds two steps for each, quasi-peak and voltage average over 25 times the RBW, each with its limit at that frequency: 56 dBµV and 46 dBµV at 1.2 MHz. The edit is a new version with a diff, approved before it runs.

6. **Compare and report.** After each fix, rerun and ask Évariste to compare runs: worst margin, most improved and most worsened readings, and whether the setup commands match, as `compare.py` checks. "Generate a test report from the last run" gives an editable PDF or HTML report, shareable to Slack.

The radiated pre-scan changes the objective to 30 MHz to 1 GHz at 120 kHz, antenna factor and cable loss, and the 15.109(a) limits, with quasi-peak alone on suspects and one run per polarization and orientation. For near-field probing, ask Évariste to center the analyzer on a flagged frequency with positive peak and max hold. Commands a profile flags as dangerous wait for your confirmation.

You no longer write or maintain the PyVISA session, `*OPC?` polling, trace parsing, JSON records, `compare.py` or a report script. The limits and margin, the review and approval of each version, and the bench, from bonding the LISN to fitting the transient limiter, stay yours.

| Step                   | Code path (this guide)                          | Galois with Évariste                                               |
| ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------ |
| Connect and driver     | PyVISA, SCPI strings from the programming guide | The analyzer's profile, or one generated from the PDF and reviewed |
| Limits                 | `emc/limits.py`                                 | Stated in the prompt, written into each suspect step               |
| Peak pre-scan          | `scan.py`: ten sweeps, max hold, corrections    | Approved sequence; max hold and corrections on the analyzer        |
| Suspects               | `remeasure()` with `QPE` and `VAV`              | Steps per suspect, each with the limit at its frequency            |
| Near-field probing     | Analyzer front panel                            | Commands in the conversation                                       |
| Record                 | JSON file per scan                              | Per-step run record with raw commands and responses                |
| Before and after       | `compare.py`                                    | Évariste compares the runs                                         |
| Report                 | Your own                                        | Generated from the run, editable, shareable                        |
| Bench setup and safety | You                                             | You                                                                |

## What can't EMC pre-compliance testing tell you?

- **Absolute radiated levels.** Without a characterized site and calibrated antenna setup, radiated numbers are trends.
- **Immunity.** ESD, radiated immunity and surge are separate tests with their own equipment.
- **The lab's final numbers.** Your analyzer, cables and room are not the lab's; correlating one unit against its lab report tells you how far apart they are.

Pre-compliance belongs wherever the hardware changes ([EVT, DVT and PVT testing](https://galoislabs.ai/blog/evt-dvt-pvt-testing)), and tying each scan to its unit and procedure is covered in [hardware test traceability](https://galoislabs.ai/blog/hardware-test-traceability).

## Frequently asked questions

### What is EMC pre-compliance testing?

Measuring a product's electromagnetic emissions with bench equipment before the formal test at an accredited lab. Near-field probes locate noise sources, a LISN and spectrum analyzer measure conducted emissions on the AC power line, and an antenna gives a radiated pre-scan. The results are engineering data: they show your margins, but they do not demonstrate compliance.

### Can I use a spectrum analyzer instead of an EMI receiver for pre-compliance?

Yes, if it offers the CISPR measurement bandwidths, ideally with a quasi-peak detector. FCC rules (47 CFR 15.35) base the limits up to 1 GHz on a CISPR quasi-peak detector and its bandwidths, and allow a peak detector at the same bandwidths instead. The accredited lab uses a measuring receiver that meets the full instrumentation requirements; the bench analyzer's job is to find problems and track fixes.

### Do I need a quasi-peak detector for EMC pre-compliance testing?

Not for the first pass. Quasi-peak is a weighted form of peak, so its reading never exceeds the peak reading of the same signal at the same bandwidth. Frequencies whose peak reading sits below the quasi-peak limit pass that limit. Conducted emissions also have an average limit 10 dB lower, so screen the peak scan against it as well. Measure quasi-peak and average only where the peak scan is near or over a limit, because quasi-peak sweeps across a whole band are slow.

### Why do I need a LISN for conducted emissions?

FCC Part 15 measures conducted emissions from 150 kHz to 30 MHz through a 50 µH/50 Ω line impedance stabilization network. The LISN gives the product a defined power-line impedance, routes its noise to a 50 Ω measurement port, and keeps noise already on the mains out of the measurement. Without one, readings depend on the building wiring.

### Can I run EMC pre-compliance scans without writing Python?

Yes. In Galois, you describe the scan to Évariste in plain English, such as a 150 kHz to 30 MHz conducted pre-scan on a Rigol DSA815 at 9 kHz RBW with a positive-peak detector, screened against the FCC Part 15 Class B average limit with a 6 dB margin. Évariste writes a draft sequence, and an engineer reviews and approves it before it can run. The approved sequence runs on the bench through galois-edge, and each step records the command sent, the raw response, the value and its limits. Évariste compares runs before and after a fix and generates a report from the results. The limits and margin, the review and approval of each version, and the bench setup, from the LISN to the transient limiter, stay yours.
