chipify

Chipify Plugin Interface

Chipify supports user plugins that extend the GUI and report output without modifying the core application. Plugins live as plain Python files in a designated directory and are discovered automatically at runtime.


Table of Contents

  1. Plugin directory
  2. PlotPlugin
  3. ReportPlugin
  4. ExpressionPlugin
  5. ExporterPlugin
  6. TabPlugin
  7. QtTabPlugin
  8. PluginContext reference
  9. Example: AI review tab (Claude API)
  10. Data reference
  11. API versioning
  12. Error behaviour
  13. Diagnostics
  14. Security note

Plugin directory

By default Chipify looks for plugins in:

~/.chipify/plugins/

Override this by setting the environment variable CHIPIFY_PLUGINS to any directory path:

export CHIPIFY_PLUGINS=/path/to/my/plugins
chipify gui

Create the directory if it does not exist:

mkdir -p ~/.chipify/plugins

Every .py file in that directory is imported when the first plugin is requested. Each file may define multiple plugin classes. Subdirectories are not scanned.


PlotPlugin

PlotPlugin adds a custom entry to the plot-mode dropdown in the GUI.

Three plot modes ship as built-in PlotPlugins: "QQ Plot (Normality)", "ECDF + Spec Limits", and "Yield vs Spec Curve" (see chipify/plot_plugins/). They register through this same interface — a user plugin whose name matches a built-in overrides the built-in.

Class attributes

Attribute Type Required Description
name str Yes Label shown in the mode selector. Must be unique across all loaded plugins. A user plugin whose name matches a built-in mode overrides the built-in.
api_version str No Plugin API version. Default "1".

Method

def draw(self, fig, ax, valid_df, stim) -> None:
Parameter Type Description
fig matplotlib.figure.Figure Blank figure. Use when you need figure-level operations (colorbars, fig.clf()).
ax matplotlib.axes.Axes The axes to draw onto.
valid_df pd.DataFrame Simulation results filtered to successful runs only (sim_error == 'None').
stim chipify.util.Stimuli Parsed YAML test specification (see Data reference).

The method must not return a value. Any exception raised inside draw() is caught, logged, and displayed as red text on the plot — it will never crash the application.

Lifecycle

draw() is called each time the user switches to your plot mode or clicks Refresh. A fresh fig and ax are provided on every call; do not cache them.

Example

# ~/.chipify/plugins/my_plot.py
import numpy as np
from chipify.plugin_loader import PlotPlugin

class GainHistogram(PlotPlugin):
    name = "Gain Histogram"

    def draw(self, fig, ax, valid_df, stim):
        if "gain_db" not in valid_df.columns:
            ax.text(0.5, 0.5, "Column 'gain_db' not found",
                    ha="center", va="center", transform=ax.transAxes)
            return
        ax.hist(valid_df["gain_db"].dropna(), bins=30, color="#3484F0", edgecolor="white")
        ax.set_xlabel("Gain (dB)")
        ax.set_ylabel("Count")
        ax.set_title("Gain Distribution")

ReportPlugin

ReportPlugin appends a custom section to Markdown and/or PDF reports generated by Chipify.

Class attributes

Attribute Type Required Description
name str Yes Section identifier. Also used as the Markdown heading if you do not include one yourself.
api_version str No Plugin API version. Default "1".

Methods

render_md

def render_md(self, valid_df, stim) -> str:
Parameter Type Description
valid_df pd.DataFrame Successful simulation rows only.
stim chipify.util.Stimuli Parsed YAML test specification.

Return a Markdown string. It is appended verbatim after the standard report body. Raise an exception to skip this section — the error is logged and the rest of the report continues normally.

render_pdf (optional)

def render_pdf(self, pdf, valid_df, stim) -> None:
Parameter Type Description
pdf matplotlib.backends.backend_pdf.PdfPages Open PDF file. Call pdf.savefig(fig) to append pages.
valid_df pd.DataFrame Successful simulation rows only.
stim chipify.util.Stimuli Parsed YAML test specification.

If not overridden, no PDF page is added.

Example

# ~/.chipify/plugins/my_report.py
import pandas as pd
from chipify.plugin_loader import ReportPlugin

class YieldSummary(ReportPlugin):
    name = "Extended Yield Summary"

    def render_md(self, valid_df, stim) -> str:
        total = len(valid_df)
        if total == 0:
            return "## Extended Yield Summary\n\nNo valid runs.\n"
        pass_cols = [c for c in valid_df.columns if c.endswith("_overall_pass")]
        lines = ["## Extended Yield Summary\n"]
        for col in pass_cols:
            tb = col.replace("_overall_pass", "")
            pct = valid_df[col].mean() * 100
            lines.append(f"- **{tb}**: {pct:.1f}% pass")
        return "\n".join(lines) + "\n"

    def render_pdf(self, pdf, valid_df, stim) -> None:
        import matplotlib.pyplot as plt
        fig, ax = plt.subplots()
        pass_cols = [c for c in valid_df.columns if c.endswith("_overall_pass")]
        yields = [valid_df[c].mean() * 100 for c in pass_cols]
        labels = [c.replace("_overall_pass", "") for c in pass_cols]
        ax.bar(labels, yields)
        ax.set_ylabel("Yield (%)")
        ax.set_title("Per-Testbench Yield")
        pdf.savefig(fig)
        plt.close(fig)

ExpressionPlugin

ExpressionPlugin injects a named derived column into the simulation results, equivalent to entries in custom_equations in settings.json but distributable as code.

Class attributes

Attribute Type Required Description
name str Yes Output column name added to the result DataFrame.
expression str Yes Expression string evaluated via SafeEvaluator.
api_version str No Plugin API version. Default "1".

Expression evaluation

Expressions are evaluated row-by-row against the result DataFrame using the same sandboxed SafeEvaluator that backs custom_equations. The evaluation context includes:

Expressions must not call import, open, exec, eval, or access dunder attributes. The sandbox blocks all of these.

Lifecycle

Expression plugins are applied automatically at the end of apply_scalar_equations(), after all explicitly configured custom_equations. If a plugin column already exists in the DataFrame, it is overwritten.

Example

# ~/.chipify/plugins/rf_metrics.py
from chipify.plugin_loader import ExpressionPlugin

class SNR(ExpressionPlugin):
    name = "snr_db"
    expression = "20 * log10(abs(signal / noise))"

class PowerEfficiency(ExpressionPlugin):
    name = "efficiency_pct"
    expression = "p_out / p_supply * 100"

ExporterPlugin

ExporterPlugin adds a new file format to the “💾 Export” menu attached to every plot (histograms, advanced analytics, transient, and each cell in the Multi-Plot Dashboard).

PNG and SVG ship as built-in exporters. To support a new format — TIFF, EPS, WebP, JPG, PDF-per-plot, anything Matplotlib’s savefig() understands — drop one of these plugins into the plugin directory.

Class attributes

Attribute Type Required Description
name str Yes Label shown in the per-plot Export menu. Must be unique across all loaded exporters. A user plugin whose name matches a built-in ("PNG Image", "SVG Vector") overrides the built-in.
extension str Yes File extension without a leading dot, e.g. "png", "svg", "tiff". Used by the save dialog.
description str No Optional one-line description. Reserved for future tooltips.
api_version str No Plugin API version. Default "1".

Method

def export(self, fig, out_path, *, theme=None) -> str:
Parameter Type Description
fig matplotlib.figure.Figure The live figure the user wants to save. Do not call plt.close(fig) — the GUI is still showing it.
out_path str Absolute path chosen via the file dialog. Write here.
theme dict \| None Active plot palette (bg, fg, grid, spine, accent, …). Built-in exporters preserve fig.get_facecolor() and ignore it; custom exporters may consult it for re-theming on save.

Return the path actually written (almost always the same as out_path).

Lifecycle

export() is called once per click. The exporter class is instantiated fresh each time, so per-instance caches do not persist across saves. If your exporter raises, the error is caught and shown to the user via a message box — no crash.

Example

# ~/.chipify/plugins/extra_exporters.py
from chipify.plugin_loader import ExporterPlugin

class WebPExporter(ExporterPlugin):
    name      = "WebP Image"
    extension = "webp"

    def export(self, fig, out_path, *, theme=None):
        fig.savefig(out_path, format="webp", dpi=200, bbox_inches="tight")
        return out_path

class TIFFExporter(ExporterPlugin):
    name      = "TIFF (300 DPI)"
    extension = "tiff"

    def export(self, fig, out_path, *, theme=None):
        fig.savefig(out_path, format="tiff", dpi=300, bbox_inches="tight",
                    facecolor=fig.get_facecolor())
        return out_path

After restarting Chipify both formats appear in the Export menu of every plot.


TabPlugin

TabPlugin adds a whole tab to the Chipify main window — the most powerful plugin type. The plugin builds its own UI (customtkinter / tkinter widgets) into a frame the host provides, and accesses simulation data exclusively through a PluginContext facade: results, datasheet specs, rendered SPICE netlists, history runs, waveforms, and a thread bridge for calling external APIs without freezing the GUI.

[!WARNING] TabPlugin is legacy. The CustomTkinter GUI has been removed; the desktop GUI is now PySide6 (Qt) and loads QtTabPlugin (Qt widgets) only — CustomTkinter TabPlugins are detected and skipped with a warning. The data-facing contract is identical: same PluginContext, same lifecycle methods. To port, change the base class to QtTabPlugin and build Qt widgets into parent (a QWidget). The other plugin types (PlotPlugin, ReportPlugin, ExpressionPlugin, ExporterPlugin) are pure-data and work unchanged. This section is kept for reference and porting — see QtTabPlugin.

Class attributes

Attribute Type Required Description
name str Yes Tab title shown in the main window. Must be unique and must not collide with a built-in tab name (Datasheet Editor, Measurements, Histograms, Advanced Analytics, Custom Equations, Transient).
api_version str No Plugin API version. Default "1".

Methods

build (required)

def build(self, parent, context) -> None:
Parameter Type Description
parent tk frame The empty tab content area. Build your widgets into it (customtkinter recommended; plain tkinter works too).
context PluginContext The data facade — see PluginContext reference. The same instance is passed to every lifecycle call, so you may keep a reference.

Called exactly once at application startup. If build() raises, the host replaces the tab content with an error panel naming your plugin and the exception — the application keeps running.

on_data_changed (optional)

def on_data_changed(self, context) -> None:

Called on the Tk main thread whenever new simulation results are loaded or the active datasheet changes (a sweep finished, a history run was selected, custom equations were applied). Refresh your widgets from context here.

on_show (optional)

def on_show(self, context) -> None:

Called when the user switches to your tab. Useful for lazy refreshes.

on_close (optional)

def on_close(self) -> None:

Called once at application shutdown. Release external resources here (network sessions, subprocesses, open files).

Lifecycle

Minimal example

# ~/.chipify/plugins/run_counter_tab.py
import customtkinter as ctk
from chipify.plugin_loader import TabPlugin

class RunCounter(TabPlugin):
    name = "Run Counter"

    def build(self, parent, context):
        theme = context.theme()
        self._lbl = ctk.CTkLabel(parent, text="No data loaded yet.",
                                 text_color=theme["text_muted"])
        self._lbl.pack(pady=40)
        self.on_data_changed(context)

    def on_data_changed(self, context):
        s = context.summary()
        self._lbl.configure(
            text=f"{s['total']} runs — {s['passed']} passing ({s['yield_pct']}%)")

A complete, runnable example ships with the repository at examples/plugins/run_info_tab.py — copy it into your plugin directory as a starting point.


QtTabPlugin

QtTabPlugin is the supported tab-plugin base, loaded by the Qt GUI (chipify, or its chipify-qt alias). The contract is identical to the legacy TabPlugin except that build(parent, context) receives a QWidget instead of a Tk frame — lay your Qt widgets into it (e.g. with a QVBoxLayout).

Everything else is the same as TabPlugin:

[!NOTE] Do not block the GUI thread. Anything slower than ~50 ms (HTTP/LLM calls, large file reads) belongs in context.run_async(...).

Minimal example

# ~/.chipify/plugins/run_counter_qt.py
from PySide6.QtWidgets import QLabel, QVBoxLayout
from chipify.plugin_loader import QtTabPlugin

class RunCounter(QtTabPlugin):
    name = "Run Counter"

    def build(self, parent, context):
        self._lbl = QLabel("No data loaded yet.", parent)
        QVBoxLayout(parent).addWidget(self._lbl)
        self.on_data_changed(context)

    def on_data_changed(self, context):
        s = context.summary()
        self._lbl.setText(
            f"{s['total']} runs — {s['passed']} passing ({s['yield_pct']}%)")

Porting an existing TabPlugin: change the base class to QtTabPlugin, swap CustomTkinter widget construction for Qt (ctk.CTkLabel(parent, ...)QLabel(..., parent) added to a layout), and leave all context.* calls untouched.


PluginContext reference

The context object handed to every TabPlugin call is the only supported way to read application data. Everything it returns is a defensive copy or plain serializable data — mutating it never affects the application — and its API is stable across chipify releases (context.api_version).

Results

Member Returns
results(valid_only=False) Copy of the loaded result DataFrame, or None. valid_only=True keeps only rows with sim_error == 'None'.
summary() {"total", "crashes", "valid", "passed", "yield_pct"} for the loaded run.

Datasheet & specs

Member Returns
datasheet_path Absolute path of the selected datasheet YAML, or None.
datasheet_text() Raw YAML text (comments included); "" when none.
specs() JSON-serializable dict of the parsed datasheet: {"datasheet", "parameters", "tests": {tb: {"measurements": {name: {min, typ, max}}, "signals", "measure"}}}. Designed to be dumped straight into an LLM prompt.

Netlists & testbenches

Member Returns
netlists() {tb_name: rendered SPICE netlist text}. Available after at least one simulation in the project; {} before.
testbench_paths() {tb_name: absolute path to the tb/*.sch schematic} for files that exist.

History & waveforms

Member Returns
history_runs() Run labels, newest first (same labels as the History dropdown).
load_run(label) DataFrame for one history run, or None.
run_meta(label) Sidecar metadata dict (yaml, duration_s, global_yield, …); {} when absent.
analysis_kinds() Which of transient / dc / ac have waveform data for the loaded run.
waveforms(kind, run_ids=None) Combined per-run waveform DataFrame (run_id + time/sweep/frequency + signals). Defaults to all valid runs — pass explicit run_ids to limit the load.

Environment

Member Returns
dirs {"in_dir", "out_dir", "tb_dir", "work_dir"} absolute paths.
theme() Active palette: plot keys (bg, fg, grid, spine, legend_*, accent) plus widget tokens (window_bg, panel, card_bg, card_border, text_muted, danger). Use these so your tab matches the app theme.
chipify_version / api_version Version strings.

Events & threading

Member Behaviour
subscribe_data_changed(callback) Calls callback() (no args, Tk main thread) on data changes. Exceptions in your callback are logged, never raised. Auto-disconnected at shutdown. Most plugins should just implement on_data_changed instead.
run_async(work, on_done=None, on_error=None) Runs work() on a background daemon thread; delivers on_done(result) / on_error(exception) back on the Tk main thread. Threading rule: work must never touch widgets; mutate UI only inside on_done / on_error. This is the supported way to call external APIs.
set_status(text, color="#3484F0") Shows a message in the application status bar.

Example: AI review tab (Claude API)

The intended flagship use of TabPlugin: a tab that sends the design’s specs, results, and SPICE netlists to an LLM and shows the analysis. The skeleton below uses the official anthropic Python SDK (pip install anthropic, key from the ANTHROPIC_API_KEY environment variable) — note how all data gathering happens through context, and the API call runs through run_async so the GUI never freezes.

# ~/.chipify/plugins/ai_review_tab.py
import json
import customtkinter as ctk
from chipify.plugin_loader import TabPlugin


class ClaudeReviewTab(TabPlugin):
    name = "AI Review"

    def build(self, parent, context):
        theme = context.theme()
        bar = ctk.CTkFrame(parent, fg_color="transparent")
        bar.pack(fill="x", padx=10, pady=10)
        self._btn = ctk.CTkButton(bar, text="Analyze results with Claude",
                                  command=lambda: self._analyze(context))
        self._btn.pack(side="left")
        self._out = ctk.CTkTextbox(parent, wrap="word")
        self._out.pack(fill="both", expand=True, padx=10, pady=(0, 10))
        self._out.insert("end", "Run a simulation, then click Analyze.")

    def _analyze(self, context):
        df = context.results(valid_only=True)
        if df is None or df.empty:
            context.set_status("AI Review: no results loaded", "#e74c3c")
            return

        # 1. Gather the payload on the main thread (cheap, copies only).
        payload = {
            "specs": context.specs(),                     # JSON-ready dict
            "result_statistics": df.describe().to_string(),
            "yield": context.summary(),
            "netlists": context.netlists(),               # {tb: spice text}
        }

        self._btn.configure(state="disabled")
        self._out.delete("1.0", "end")
        self._out.insert("end", "Asking Claude…\n")

        # 2. Call the API off-thread; 3. render the answer back on the UI.
        context.run_async(
            lambda: self._ask_claude(payload),
            on_done=self._show_answer,
            on_error=lambda exc: self._show_answer(f"Request failed: {exc}"),
        )

    @staticmethod
    def _ask_claude(payload) -> str:
        # Runs on a worker thread — no widget access here!
        import anthropic
        client = anthropic.Anthropic()  # key from ANTHROPIC_API_KEY
        response = client.messages.create(
            model="claude-opus-4-8",
            max_tokens=16000,
            thinking={"type": "adaptive"},
            system=("You are an analog IC design reviewer. You receive a "
                    "chipify datasheet spec, Monte-Carlo/corner results, and "
                    "SPICE netlists. Point out failing specs, marginal Cpk, "
                    "and likely design causes. Be concrete."),
            messages=[{
                "role": "user",
                "content": json.dumps(payload, default=str),
            }],
        )
        return next((b.text for b in response.content if b.type == "text"), "")

    def _show_answer(self, text):
        self._btn.configure(state="normal")
        self._out.delete("1.0", "end")
        self._out.insert("end", str(text))

Tips for production use:


Data reference

valid_df — result DataFrame

Each row is one simulation run. Only successful runs are included (sim_error == 'None').

Common columns:

Column pattern Description
Sweep parameter names One column per parameter defined in parameters: in the YAML (e.g. temp, vdd).
Measurement names One column per values: entry in each testbench (e.g. gain_db, idd_ua).
<tb_name>_overall_pass Boolean: whether all specs passed for that testbench.
global_pass Boolean: all testbenches passed.
sim_error Always "None" in valid_df.
simulation_duration_s_total Wall-clock seconds for the full run (first row only).

Custom equations (from settings.json or ExpressionPlugin) append additional columns after the simulation columns.

stim — parsed YAML specification

stim is a chipify.util.Stimuli object. Key attributes:

Attribute Type Description
stim.params dict[str, list] Parameter name → list of sweep values.
stim.tests list[Test] One Test per testbench block in the YAML.
stim.tests[i].tb_path str Testbench identifier (used as column prefix).
stim.tests[i].value_lst list[Value] Measurement specs (Value.name, Value.vmin, Value.vtyp, Value.vmax).
stim.tests[i].analyses list[Analysis] Declared signal captures (.kind ∈ transient/dc/ac, .signals).
stim.tests[i].measure dict[str, str] measure: expressions from the YAML.

TabPlugins should prefer context.specs() over walking stim directly — it returns the same information as a stable, JSON-serializable dict.


API versioning

Every plugin class may declare:

api_version = "1"

This is currently informational. When a future Chipify release introduces breaking changes to the plugin interface it will increment the current API version. Plugins targeting an older version will log a warning on load but will continue to function as long as the old interface remains compatible.

There is no hard rejection based on api_version — warnings only.


Error behaviour

Situation What happens
Syntax error in plugin file File is skipped. Full traceback logged at WARNING level. Other plugins still load.
draw() raises Exception caught. Error message rendered on the plot in red.
render_md() raises Section skipped. Error logged. Rest of the report is unaffected.
render_pdf() raises PDF page skipped. Error logged.
ExpressionPlugin expression fails Column set to NaN for all rows. Warning logged.
ExporterPlugin.export() raises Save is cancelled; error message shown in a dialog. App remains running.
TabPlugin.build() raises The plugin’s tab shows an error panel with the exception; full traceback logged. Other tabs unaffected.
TabPlugin.on_data_changed() / on_show() / on_close() raises Call skipped for that plugin; exception logged.
A run_async worker or callback raises Logged; on_error invoked for worker failures. The GUI never sees the exception.
TabPlugin.name collides with a built-in tab Plugin skipped. Warning logged.
Two plugins with the same name Second plugin is skipped. Warning logged. For exporters, a user plugin overrides a built-in of the same name.

Chipify never crashes due to a bad plugin.


Diagnostics

from chipify.plugin_loader import list_plugins
import pprint
pprint.pprint(list_plugins())

Output example:

{
    'expression': [{'api_version': '1', 'name': 'snr_db'},
                   {'api_version': '1', 'name': 'efficiency_pct'}],
    'exporter':   [{'api_version': '1', 'name': 'PNG Image'},
                   {'api_version': '1', 'name': 'SVG Vector'},
                   {'api_version': '1', 'name': 'WebP Image'}],
    'plot':       [{'api_version': '1', 'name': 'QQ Plot (Normality)'},
                   {'api_version': '1', 'name': 'ECDF + Spec Limits'},
                   {'api_version': '1', 'name': 'Yield vs Spec Curve'},
                   {'api_version': '1', 'name': 'Gain Histogram'}],
    'report':     [{'api_version': '1', 'name': 'Extended Yield Summary'}],
    'tab':        [{'api_version': '1', 'name': 'AI Review'}],
}

You can also call reload_plugins() to force re-discovery without restarting the application (useful during plugin development):

from chipify.plugin_loader import reload_plugins
reload_plugins()

To check the active plugin directory:

from chipify.plugin_loader import plugin_dir
print(plugin_dir())

Security note

Plugin files are executed as full Python with the same privileges as the Chipify process. Treat plugin files the same way you would treat shell scripts — only load plugins from sources you trust. The CHIPIFY_PLUGINS environment variable can point to any directory, so be careful when running Chipify in shared environments.

The ExpressionPlugin.expression string is evaluated inside the SafeEvaluator sandbox (asteval) which blocks import, open, exec, and dunder access. It is safe to load expressions from untrusted sources. The plugin file itself, however, is not sandboxed.