Custom variation and measurement

Two resistors drawn together with a recorded label, and a measurement defined by a function.

python examples/07_custom.py
# %% [markdown]
# # Custom variations and measurements
#
# Define a joint variation and a measurement function. Functions used by workers must
# live at module level. Also runnable as `python examples/07_custom.py`.
# Jupyter uses one worker because functions defined in cells cannot be imported by spawned workers.
# %%

import sys
from pathlib import Path

import numpy as np

examples_dir = next(
    (root / "examples" for root in (Path.cwd(), *Path.cwd().parents)
     if (root / "examples" / "rc_study.py").is_file()),
    None,
)
if examples_dir is not None:
    sys.path.insert(0, str(examples_dir))

# %%
from rc_study import circuit, config  # noqa: E402

from spicefault import Experiment, Measurement  # noqa: E402
from spicefault.faults import ParametricFault  # noqa: E402
from spicefault.variation import Draw, JointVariation, ToleranceVariation  # noqa: E402


def matched_resistors(rng, netlist):
    """R1 and R2 from one reel: they share most of their deviation and differ a little.

    The type of the reel is drawn first and recorded as a label of the sample.
    """
    grade = str(rng.choice(["standard", "precision"]))
    spread = {"standard": 0.05, "precision": 0.005}[grade]
    common = 1.0 + spread * rng.uniform(-1, 1)
    values = {
        (name, "value"): netlist.value(name) * common * (1.0 + 0.001 * rng.uniform(-1, 1))
        for name in ("R1", "R2")
    }
    return Draw(values, {"resistor_grade": grade})


# %% [markdown]
# ## A custom frequency-response measurement
# %%
def corner_frequency(plot):
    """Frequency at which the response has fallen 3 dB from its low-frequency value [Hz]."""
    frequency = plot["frequency"].real
    gain = np.abs(plot["v(out)"])
    below = np.flatnonzero(gain < gain[0] / np.sqrt(2.0))
    return float(frequency[below[0]]) if len(below) else float(frequency[-1])


# %%
if __name__ == "__main__":
    worker_count = 1 if "ipykernel" in sys.modules else 2
    experiment = Experiment(
        circuit,
        config=config,
        faults=[ParametricFault("C1", deviation=0.5)],
        variations=[
            JointVariation("resistors", (("R1", "value"), ("R2", "value")), matched_resistors),
            ToleranceVariation("C1", 0.05),
        ],
        measurements=[
            Measurement.value("v(out)", name="dc"),
            Measurement.custom("corner", corner_frequency, analysis="ac"),
        ],
        samples=6,
        seed=3,
    )
    table = experiment.run(workers=worker_count).to_frame()
    columns = ["fault_id", "resistor_grade", "p_R1_value", "p_R2_value", "dc", "corner"]
    print(table[columns].round(3).to_string(index=False))
    # matched resistors keep the divider ratio, whatever their grade
    print("\nspread of the DC gain:", f"{table['dc'].std():.5f}")
    print("recorded in the definition of the experiment:")
    print(" ", experiment.metadata()["variations"][0])