Metadata-Version: 2.4
Name: analogpy
Version: 0.1.2
Summary: Analog circuit IR (Intermediate Representation) and Spectre netlist generator
Author-email: Gaofeng Fan <circuitmuggle@gmaigmaill.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/circuitmuggle/analogpy
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Provides-Extra: visualization
Requires-Dist: schemdraw>=0.18; extra == "visualization"
Requires-Dist: reportlab>=4.0; extra == "visualization"
Requires-Dist: pypdf>=4.0; extra == "visualization"
Dynamic: license-file

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 [tmux-ssh](https://github.com/circuitmuggle/tmux-ssh)

3. **Parse simulation results** (planned)
   - Read PSF/nutbin files
   - Expose data as numpy arrays / pandas DataFrames
   - Enable Python-native post-processing

4. **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 [tmux-ssh](https://github.com/circuitmuggle/tmux-ssh)
- **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   │  │   numpy     │  │    matplotlib       │  │
│  │  (netlist)  │  │   scipy     │  │    plotly           │  │
│  │  (parse)    │  │   pandas    │  │    (visualization)  │  │
│  │  (expose)   │  │  (analysis) │  │                     │  │
│  └──────┬──────┘  └──────┬──────┘  └─────────┬──────────-┘  │
└─────────┼────────────────┼───────────────────┼──────────────┘
          │                │                   │
          ▼                ▼                   ▼
    ┌──────────┐    ┌──────────────┐    ┌───────────┐
    │ Spectre  │    │ Post-process │    │  Plots    │
    │ Netlist  │    │ (FFT, etc.)  │    │ PNG/HTML  │
    └──────────┘    └──────────────┘    └───────────┘
```

## Roadmap

- 0.1.x AST + netlist generation ✅
- 0.2.x Result parser + data exposure
- 0.3.x Optimization / AI hooks
- 1.0.0 Stable IR

## Installation

```bash
pip install -e .
```

## Quick Start

```python
from analogpy import Circuit, nmos, pmos, generate_spectre
from analogpy.devices import vsource

# Define a reusable inverter circuit
inv = Circuit("inverter", ports=["in", "out", "vdd", "gnd"])
inv.add(nmos("MN", d=inv.net("out"), g=inv.net("in"),
             s=inv.net("gnd"), b=inv.net("gnd"), w=1e-6, l=180e-9))
inv.add(pmos("MP", d=inv.net("out"), g=inv.net("in"),
             s=inv.net("vdd"), b=inv.net("vdd"), w=2e-6, l=180e-9))

# Create top-level circuit (no ports = top level)
top = Circuit("tb_inverter", ports=[])
vin = top.net("vin")
vout = top.net("vout")
top.instantiate(vsource, instance_name="I_Vdd", p=top.net("vdd"), n=gnd, dc=1.8)
vdd = top.net("vdd")
gnd = top.gnd()

# Instantiate the inverter
top.instantiate(inv, "X1", **{"in": vin, "out": vout, "vdd": vdd, "gnd": gnd})

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

## 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/05_result_processing.py` - Parse results and plot (planned)

## 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.instantiate()`
- **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

```python
from analogpy import Testbench, DC, AC, Transient
from analogpy.devices import vsource

tb = Testbench("tb_amp")
tb.instantiate(vsource, instance_name="I_Vdd", p=tb.net("vdd"), n=tb.gnd(), dc=1.8)
tb.net("vdd")
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))
```

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

```python
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
- **Passives**: `resistor()`, `capacitor()`, `inductor()`
- **Sources**: `vsource()`, `isource()`, `vpulse()`, `vsin()`
- **Controlled sources**: `vcvs()`, `vccs()`, `ccvs()`, `cccs()`
- **Other**: `diode()`

### Phase 5: SpectreCommand (Implemented)

- **Command builder**: Generate spectre commands without execution
- **Configurable**: Accuracy, threads, timeouts, include paths
- **Presets**: Liberal (fast), conservative (robust), moderate

```python
from analogpy import SpectreCommand

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

# User executes via shell or tmux-ssh
```

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

```python
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 tmux-ssh {}
```

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

```python
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.

```bash
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** | Left (default) | All other names |

#### Customizing Port Locations

Override the auto-inference using `port_overrides`:

```python
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

```python
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.

### Phase 8: Result Parsing (Planned)

- **Parse PSF/nutbin**: Read Spectre output files
- **Expose as Python data**: numpy arrays, pandas DataFrames
- **Display config**: Separate from save config
- **Validation**: Warn if display signal not in saved signals

```python
# Planned API
from analogpy.results import load_results

results = load_results("/sim/ldo_pvt/tt_v1.8_t27/psf")

# Point query
vout_dc = results.dc["X_OTA.vout"]

# Waveform as numpy array
vout_tran = results.tran["vout"]  # Returns (time, values) arrays

# At specific time
vgs_at_10ns = results.tran["M1:vgs"].at(10e-9)

# Use Python for analysis
import numpy as np
from scipy.fft import fft

spectrum = fft(vout_tran.values)  # numpy/scipy does the 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
└── results/        # Result parsing (planned)
```

## Design Principles

1. **Netlist-focused**: Generate netlists, expose results - 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

## Testing

```bash
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)
```bash
# 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`:
```yaml
simulator:
  mode: remote
  remote:
    spectre_path: /tools/cadence/SPECTRE231/bin/spectre
    workdir: /tmp/analogpy
```

Option 2: **Local Spectre** (if installed on your machine)
```bash
# Spectre in PATH
which spectre  # Should return path

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

Option 3: **Remote via tmux-ssh** (auto-detected if config exists)
```bash
# Install tmux-ssh
pip install tmux-ssh

# Configure once (credentials are saved to ~/.tmux_ssh_config)
tmux-ssh 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 tmux-ssh (reads ~/.tmux_ssh_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
