Faults and coverage

Fault types and their records, a fault universe generated from rules, its coverage matrix, the faults left out with their reasons, and the parametric faults that lie inside the tolerance band. Nothing is simulated.

python examples/02_faults_and_coverage.py
# %% [markdown]
# # Faults and coverage
#
# Faults as objects, a fault universe from rules, and what a campaign covers.
# Nothing is simulated here. Also runnable as `python examples/02_faults_and_coverage.py`.
# %%

import json
import sys
from pathlib import Path

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))

# %% [markdown]
# ## Faults as recorded primitive changes
# %%
from rc_study import circuit, population  # noqa: E402

from spicefault.faults import (  # noqa: E402
    CompositeFault,
    Fault,
    FaultSet,
    FaultUniverse,
    LeakageFault,
    OpenCircuit,
    ParametricFault,
    ShortCircuit,
    leakage_rule,
    open_rule,
    parametric_rule,
    short_rule,
)

faults = FaultSet(
    [
        OpenCircuit("R1"),
        ShortCircuit("C1"),
        LeakageFault("C1", 1e6, r_min=1e4, r_max=1e8),
        ParametricFault("R2", deviation=-0.2),
        # an ageing capacitor: less capacitance and more series resistance, one cause
        CompositeFault(
            "capacitor_ageing",
            [ParametricFault("C1", factor=0.7), OpenCircuit("C1", r_open=50.0)],
            magnitude=0.3,
            unit="capacitance loss",
        ),
    ]
)
for fault in faults:
    severity = "-" if fault.severity is None else f"{fault.severity.value:.2f}"
    print(f"{fault.fault_id:28s} type {fault.fault_type:18s} severity {severity}")

print("\nthe record of one fault:")
record = faults["R2:parametric:-0.2"].metadata()
print(json.dumps(record, indent=2))
print("rebuilt from its record, it is the same fault:", Fault.from_metadata(record) == faults[3])

# %% [markdown]
# ## A universe generated from rules
# %%
universe = FaultUniverse(
    circuit,
    [
        open_rule(),
        short_rule(),
        parametric_rule([-0.2, -0.05, 0.05, 0.2]),
        leakage_rule([1e5, 1e6, 1e7], kinds="C"),
    ],
)
universe.exclude("R2:short", reason="ties the output to ground: same effect as C1:short")
print(f"\n{len(universe)} fault conditions in the universe, {len(universe.selected())} selected")
print("\nconditions to simulate, by component and fault type:")
print(universe.coverage_matrix())
print("\nleft out, with the reason:")
print(universe.exclusions().to_string(index=False))
print(f"\nstructural coverage: {universe.coverage():.0%}")
print("components outside the fault model:", universe.metadata()["components_without_faults"])

# %% [markdown]
# ## Parametric faults inside the tolerance band
# %%
overlap = universe.selected().tolerance_overlap(population, circuit)
inside = overlap[overlap["inside_fraction"] > 0]
print("\nparametric faults partly inside the tolerance band of their component:")
print(inside[["fault_id", "tolerance", "inside_fraction"]].to_string(index=False))