Skip to main content

Analog circuit IR (Intermediate Representation) and Spectre netlist generator

Project description

License: Apache-2.0

analog-py

Python DSL + AST + Codegen for Analog Circuit Design and Netlist Generation.

Project Goals

analogpy is a Python library for generating circuit netlists. It bridges the gap between Python programming and analog circuit simulation.

What analogpy DOES:

  1. Generate netlists (MVP: Spectre, future: ngspice)

    • Circuit topology in Python
    • Hierarchical circuits
    • Testbench with analyses
  2. Build simulation commands (not execute)

    • SpectreCommand builder with configurable options
    • User executes via shell or tmux4ssh
  3. Make Python loop design easy

    • PVT corners: Python loop generates N netlists
    • Monte Carlo: Python loop with different seeds
    • Parameter sweeps: Python variables directly in netlist

What analogpy does NOT do:

  • Job submission: Use shell or tmux4ssh
  • Result parsing: Use psf-utils for PSF ASCII files
  • Heavy analysis: Use numpy, scipy (FFT, filtering, etc.)
  • Visualization: Use matplotlib, plotly (analogpy provides helpers)
  • Replace Cadence ADE: analogpy is CLI/script-first, not GUI

Design Philosophy

┌─────────────────────────────────────────────────────────────┐
│                      Python Script                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  analogpy   │  │  psf-utils  │  │    matplotlib       │  │
│  │  (netlist)  │  │   numpy     │  │    plotly           │  │
│  │  (command)  │  │   scipy     │  │    (visualization)  │  │
│  │             │  │  (analysis) │  │                     │  │
│  └──────┬──────┘  └──────┬──────┘  └─────────┬──────────-┘  │
└─────────┼────────────────┼───────────────────┼──────────────┘
          │                │                   │
          ▼                ▼                   ▼
    ┌──────────┐    ┌──────────────┐    ┌───────────┐
    │ Spectre  │    │ Post-process │    │  Plots    │
    │ Netlist  │    │ (FFT, etc.)  │    │ PNG/HTML  │
    └──────────┘    └──────────────┘    └───────────┘

Roadmap

  • 0.1.x AST + netlist generation ✅
  • 0.2.x Optimization / AI hooks
  • 1.0.0 Stable IR

Installation

pip install -e .

Quick Start

from analogpy import Circuit, nmos, pmos, vsource, generate_spectre


class Inverter(Circuit):
    """CMOS Inverter cell."""

    def __init__(self, w_n: float = 1e-6, w_p: float = 2e-6, l: float = 180e-9):
        # Ports with optional direction using colon syntax:
        # "inp:input" = input, "out:output" = output, default = inout
        super().__init__("inverter", ports=["inp:input", "out:output", "vdd", "vss"])

        self.add_instance(nmos, "MN", d=self.add_net("out"), g=self.add_net("inp"),
                          s=self.add_net("vss"), b=self.add_net("vss"), w=w_n, l=l)
        self.add_instance(pmos, "MP", d=self.add_net("out"), g=self.add_net("inp"),
                          s=self.add_net("vdd"), b=self.add_net("vdd"), w=w_p, l=l)


# Create inverter with default sizing
inv = Inverter()

# Create top-level circuit (no ports = top level)
top = Circuit("tb_inverter", ports=[])

# add_net() and net() are equivalent — use whichever you prefer
vin_net  = top.net("vin")       # short form
vout_net = top.add_net("vout")  # explicit form
vdd_net  = top.add_net("vdd")
gnd      = top.add_gnd()        # Global ground "0"; top.gnd() also works

# add_instance() and instance() are equivalent
vdd_inst = top.add_instance(vsource, "I_Vdd", p=vdd_net, n=gnd, dc=1.8)
x1_inst  = top.instance(inv, "X1", inp=vin_net, out=vout_net, vdd=vdd_net, vss=gnd)

# Generate Spectre netlist
netlist = generate_spectre(top)
print(netlist)

Note on port naming: Avoid using Python reserved keywords (in, for, class, etc.) as port names. For example, add_instance(inv, "X1", in=vin) is a syntax error because in is a reserved word. Use inp instead. If you must match an existing netlist that uses in as a port name, use dict unpacking as a workaround: add_instance(inv, "X1", **{"in": vin, "out": vout, "vdd": vdd, "vss": gnd})

Examples

See the examples/ folder for complete workflows:

  • examples/01_inverter_basic.py - Simple inverter netlist
  • examples/02_ota_testbench.py - OTA with DC/AC analysis
  • examples/03_pvt_sweep.py - PVT corner sweep with Python loop
  • examples/04_monte_carlo.py - Monte Carlo with Python loop
  • examples/06_oled_dc.py - OLED DC simulation with Verilog-A LUTs (includes SpectreCommand reference)
  • examples/07_oled2.py - Series OLED testbench using function-built cells

Features

Phase 1: Core Hierarchy (Implemented)

  • Circuit: Reusable circuit blocks with defined ports (maps to Spectre subckt)
  • Aliases: Subcircuit and Subckt are aliases for Circuit
  • Instantiation: Hierarchical design with circuit.add_instance()
  • Nested hierarchy: Circuits can contain other circuits
  • Top-level: Use Circuit("name", ports=[]) or Testbench for simulation top

Phase 2: Testbench & Analysis (Implemented)

  • Testbench: Test environment extending Circuit with simulation setup
  • Analysis classes: DC, AC, Transient, Noise, STB
  • Simulator options: Temperature, tolerances, convergence settings
  • Behavioral models: Verilog-A include support
from analogpy import Testbench, DC, AC, Transient
from analogpy.devices import vsource

tb = Testbench("tb_amp")
vdd = tb.add_net("vdd")   # preferred; tb.net("vdd") is an alias
gnd = tb.add_gnd()        # preferred; tb.gnd() is an alias
vdd_inst = tb.add_instance(vsource, instance_name="I_Vdd", p=vdd, n=gnd, dc=1.8)
tb.set_temp(27)
tb.add_analysis(DC())
tb.add_analysis(AC(start=1, stop=1e9, points=100))
tb.add_analysis(Transient(stop=1e-6))

Analysis extras and SimulatorOptions

All analysis classes and SimulatorOptions support an extras dict for arbitrary Spectre parameters not covered by named fields:

from analogpy import Transient, DC

# cmin is a named field on Transient (minimum capacitance per node for convergence)
tran = Transient(stop=1e-6, cmin=1e-18)

# Use extras for any other Spectre analysis parameter
tran = Transient(stop=1e-6, extras={"errpreset": "conservative", "method": "euler"})
dc = DC(extras={"homotopy": "all"})

SimulatorOptions — tolerance fields (reltol, vabstol, iabstol, gmin) default to None and are not emitted, letting the command-line accuracy mode (++aps, +aps) control them. Set explicitly only when you need to override:

tb = Testbench("tb_amp")
tb.simulator_options.reltol = 1e-6       # Override tolerance
tb.simulator_options.gmin = 1e-15        # Tighter gmin
tb.simulator_options.extras = {"rforce": 1, "pivotdc": "yes"}  # Convergence helpers

temp vs tnom:

  • temp — circuit simulation temperature (varies in PVT sweeps)
  • tnom — temperature at which device model parameters were measured/extracted (usually fixed to match PDK characterization, e.g. 27 or 25)

Phase 3: SaveConfig (Implemented)

  • Hierarchical saves: Define saves at block level, apply with prefix
  • Tagged signals: Filter saves by category
  • Testbench control: Override, include, exclude saves
from analogpy import SaveConfig

# Define saves for OTA block
ota_saves = (SaveConfig("ota")
    .voltage("out", "tail", tag="essential")
    .op("M1:gm", "M2:gm", tag="op_params"))

# In testbench, apply with hierarchy prefix
tb.save(ota_saves.with_prefix("X_LDO.X_OTA"))

Phase 4: Device Primitives (Implemented)

  • MOSFETs: nmos(), pmos() with nf support
  • BJT/JFET: bjt(), jfet() for bipolar and junction FETs
  • Passives: resistor(), capacitor(), inductor(), mutual_inductor()
  • Sources: vsource(), isource()
  • Controlled sources: vcvs(), vccs(), ccvs(), cccs()
  • Other: diode(), iprobe(), port() (for S-parameter)

Phase 5: SpectreCommand (Implemented)

  • Command builder: Generate spectre commands without execution
  • Minimal defaults: Only emits flags you explicitly set
  • Configurable: Accuracy, threads, output format, include paths
  • Presets: Liberal (fast), conservative (robust), moderate
from analogpy import SpectreCommand

cmd = (SpectreCommand("input.scs")
    .accuracy("liberal")
    .threads(16)
    .include_path("/path/to/models")
    .build())

# User executes via shell or tmux4ssh

SpectreCommand Options Reference

Method Spectre Flag Description
.output_format(fmt) -format Raw data format: "psfascii" (default), "psfbin", "psfxl", "psfbinf", "nutbin", "nutascii", "sst2", "fsdb", "fsdb5", "wdf", "uwi", "tr0ascii". PSF ASCII files can be read with psf-utils
.accuracy(level, mode) ++aps, +aps, +errpreset Error tolerance and acceleration (see below)
.threads(n) +mt=N Number of parallel threads (max 64)
.include_path(*paths) -I Add include paths for model files
.log_file(path) +log Log file path (default: Spectre writes <netlist>.log)
.raw_dir(path) -raw Raw output directory (default: Spectre writes in current dir)
.ahdl_libdir(path) -ahdllibdir Compiled Verilog-A model cache directory (default: raw output dir)
.timeout(seconds) +lqtimeout License queue timeout — abort if license not acquired in time
.max_warnings(n) -maxw Max warnings before Spectre aborts
.max_notes(n) -maxn Max informational notes before suppression
.logstatus() +logstatus Enable status logging for monitoring simulation progress
.flag("+escchars") +escchars Allow backslash-escaped characters in paths/strings

Accuracy modes.accuracy(level, mode):

  • level: "liberal" (fast), "moderate", "conservative" (accurate)
  • mode (optional, default "++aps"):
    • "++aps" — Uses a different time-step control algorithm for improved performance while satisfying error tolerances. Emits ++aps=<level>
    • "+aps" — Spectre APS mode, a different simulator engine from base Spectre. Emits +aps=<level>
    • "errpreset" — Base Spectre error preset only, no APS acceleration. Emits +errpreset=<level>
# Examples
.accuracy("liberal")              # ++aps=liberal (default mode)
.accuracy("liberal", "+aps")      # +aps=liberal
.accuracy("moderate", "errpreset") # +errpreset=moderate

Note: Only .output_format() is emitted by default (-format psfascii). All other flags are opt-in — if not called, they are not included in the generated command, letting Spectre use its own defaults.

Phase 6: SimulationBatch (Implemented)

  • PVT sweeps: Process/Voltage/Temperature corners
  • Monte Carlo: Generate N runs with different seeds
  • Runner scripts: Python scripts with CLI configuration
from analogpy import SimulationBatch

# Python loop generates multiple netlists
batch = SimulationBatch("ldo_pvt", "/sim/ldo_pvt")
batch.pvt_sweep(make_tb_ldo, corners=[
    {"process": "tt", "voltage": 1.8, "temp": 27},
    {"process": "ff", "voltage": 1.98, "temp": -40},
    {"process": "ss", "voltage": 1.62, "temp": 125},
])
batch.command_options(accuracy="liberal", threads=16)
batch.generate()
batch.write_runner("run_pvt.py")

# User runs: python run_pvt.py commands | parallel tmux4ssh {}

Phase 7: PDK Infrastructure (Implemented)

  • PDK loader: Load PDK configuration by name
  • Multi-source config: Project, user, environment variables
  • NDA-safe: PDK files never included in package
from analogpy.pdk import PDK

pdk = PDK.load("tsmc28")  # Loads from config
mn1 = pdk.nmos("M1", d=vout, g=vin, s=gnd, b=gnd, w=1e-6, l=28e-9, nf=4)

Visualization Module (Experimental)

Generate schematic symbols and block diagrams for circuit documentation.

pip install analogpy[visualization]  # Requires schemdraw, reportlab, pypdf

Port Type Inference

The visualization module automatically infers port placement on symbols based on naming conventions:

Port Type Position Pattern Examples
POWER Top vdd, avdd, vcc, pwr, anode, *_vdd
GROUND Bottom vss, gnd, elvss, cathode, *_gnd
INPUT Left in, clk, en, rst, din, sel, *_in
OUTPUT Right out, q, y, dout, *_out
INOUT Left (below inputs) io, sda, scl, data, bus
UNKNOWN Right (below outputs) All other names

Customizing Port Locations

Override the auto-inference using port_overrides:

from analogpy.visualization import draw_cell_symbol, PortType
import schemdraw

# Define your custom port types
port_overrides = {
    "BIAS": PortType.INPUT,      # Force BIAS to left side
    "MONITOR": PortType.OUTPUT,   # Force MONITOR to right side
}

# Draw symbol with overrides
with schemdraw.Drawing() as d:
    d.config(unit=1, fontsize=10)
    positions = draw_cell_symbol(
        d, "my_cell",
        ports=["VDD", "VSS", "IN", "OUT", "BIAS", "MONITOR"],
        port_overrides=port_overrides
    )
    d.save("my_cell.png")

Standalone Symbol Generation

from analogpy.visualization import create_cell_symbol_standalone

# Quick way to generate a symbol image
d = create_cell_symbol_standalone("oled_cell", ["ANODE", "ELVSS"])
d.save("oled_symbol.png")

Note: This module is experimental. Block diagram connection routing still needs work.

Architecture

analogpy/
├── circuit.py      # Circuit (Subcircuit, Subckt are aliases), Net, Instance
├── devices.py      # nmos, pmos, resistor, capacitor, etc.
├── spectre.py      # Spectre netlist generation
├── testbench.py    # Testbench class
├── analysis.py     # DC, AC, Transient, Noise, STB
├── save.py         # SaveConfig for probe management
├── command.py      # SpectreCommand builder
├── batch.py        # SimulationBatch for PVT/MC
└── pdk/            # PDK loader infrastructure

Design Principles

  1. Netlist-focused: Generate netlists - that's it
  2. Python-native: Use Python variables, loops, data structures
  3. Don't reinvent: FFT? Use scipy. Plots? Use matplotlib.
  4. CLI-first: No GUI, scripts and commands
  5. AI-friendly: Simple patterns for LLM generation

Naming Conventions

Following PEP 8:

  • Files/modules: snake_caseoled_1rc.py, circuit.py
  • Classes: PascalCaseOled1RC, Circuit, Testbench
  • Functions/variables: snake_caseadd_instance(), generate_spectre()
  • Device primitives: lowercasenmos, pmos, resistor, cccs

Example: from cells.oled_1rc import Oled1RC — file is snake_case, class is PascalCase.

Testing

pytest tests/ -v

Simulator Integration Tests

Some tests require a working Spectre simulator. These are marked with @pytest.mark.simulator and will be automatically skipped if no simulator is available.

Test levels:

  1. Syntax checks - Always run, use Python-based validation
  2. Basic simulation - Requires simulator, runs actual simulations
  3. Result validation - Compares results against expected values

Setting up simulator access:

Option 1: Config file (recommended for remote simulation)

# Copy template to ~/.analogpy/
mkdir -p ~/.analogpy
cp config.yaml.template ~/.analogpy/config.yaml

# Edit the config file to set remote spectre path
# Uncomment and modify the settings you need

Example ~/.analogpy/config.yaml:

simulator:
  mode: remote
  remote:
    spectre_path: /tools/cadence/SPECTRE231/bin/spectre
    workdir: /tmp/analogpy

Option 2: Local Spectre (if installed on your machine)

# Spectre in PATH
which spectre  # Should return path

# Or set explicit path
export SPECTRE_PATH=/path/to/spectre

Option 3: Remote via tmux4ssh (auto-detected if config exists)

# Install tmux4ssh
pip install tmux4ssh

# Configure once (credentials are saved to ~/.tmux4ssh_config)
tmux4ssh user@your-spectre-server.com

# Now pytest will automatically use remote execution
pytest tests/test_simulation.py -v

Configuration precedence:

  1. ~/.analogpy/config.yaml (user config file)
  2. Environment variables (override config file)
  3. Local Spectre (PATH or SPECTRE_PATH)
  4. Remote via tmux4ssh (reads ~/.tmux4ssh_config)
  5. Skip with helpful message

Environment variables:

Variable Description Default
SPECTRE_PATH Path to local spectre binary Auto-detect from PATH
ANALOGPY_WORKDIR Working directory for simulation files /tmp/analogpy
ANALOGPY_SKIP_SIMULATION Set to "1" to skip all simulation tests Disabled

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

analogpy-0.2.10.tar.gz (99.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

analogpy-0.2.10-py3-none-any.whl (97.5 kB view details)

Uploaded Python 3

File details

Details for the file analogpy-0.2.10.tar.gz.

File metadata

  • Download URL: analogpy-0.2.10.tar.gz
  • Upload date:
  • Size: 99.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for analogpy-0.2.10.tar.gz
Algorithm Hash digest
SHA256 8fd25d8420689f45244ec39caab76fbc310be39bf8b17d15437f4bbb7e004584
MD5 fd7a2c1a40d7823b967f44f634e69284
BLAKE2b-256 64077f01f70abaddd2b4e320437ed6b262bb667e463110ad2660b47164fbb744

See more details on using hashes here.

File details

Details for the file analogpy-0.2.10-py3-none-any.whl.

File metadata

  • Download URL: analogpy-0.2.10-py3-none-any.whl
  • Upload date:
  • Size: 97.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for analogpy-0.2.10-py3-none-any.whl
Algorithm Hash digest
SHA256 fb6d2eafa17b38bd2e5c156a51f3913ed472e604ef16a18a837b110942f10648
MD5 525624fbf7c4c32f4a738039afc39241
BLAKE2b-256 5790ad045059b1ad635cccda911613efcb7cbfc233973de4afd4b25456736d31

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page