gautamsgev/ELectrical_Metaopenenv
CircuitSynth-SquareWave
An OpenEnv-compliant RL environment for LLM-based electronic circuit synthesis using ngspice SPICE simulation.
The agent learns to design transistor-based astable oscillator circuits that generate target square waveforms by incrementally placing and connecting components from a fixed library.
Overview
┌─────────────┐ action ┌───────────────────┐ SPICE netlist ┌──────────────┐
│ RL Policy │ ──────────→ │ CircuitSynthEnv │ ──────────────→ │ ngspice │
│ (any algo) │ │ (OpenEnv API) │ │ (simulator) │
└─────────────┘ ←──────────── └───────────────────┘ ←────────────── └──────────────┘
obs + reward circuit graph waveform dataCircuit family: NPN BJT cross-coupled astable multivibrator (transistor-RC square-wave oscillator).
Objective: Train an RL agent to discover circuits that produce a target square wave with correct frequency, duty cycle, amplitude, and stability — using as few components as possible.
Quick Start
Installation
git clone https://github.com/your-org/circuitsynth-squarewave
cd circuitsynth-squarewave
pip install -e .For real SPICE simulation, install ngspice:
sudo apt-get install ngspice # Ubuntu / Debian
brew install ngspice # macOS (Homebrew)Without ngspice, the environment uses a built-in mock simulator that estimates waveform quality from circuit topology. Set mock_sim=True.
Basic Usage
from circuitsynth import CircuitSynthEnv
# Create environment (mock mode — no ngspice required)
env = CircuitSynthEnv(task_id="squarewave-easy", seed=42, mock_sim=True)
# OpenEnv API
obs, info = env.reset()
for step in range(30):
action = env.action_space.sample() # random policy
obs, reward, terminated, truncated, info = env.step(action)
print(f"Step {step}: reward={reward:.4f} components={info['n_components']}")
if terminated or truncated:
break
# Full MDP state (for debugging / logging)
state = env.state()
print(state["reward_decomposition"])Scripted Human-Readable Actions
# Use dict-style actions for scripting / debugging
obs, info = env.reset()
env.step_dict({"action_type": "ADD_COMPONENT", "component_type": "VSOURCE",
"value_idx": 5, "node_a": "VCC", "node_b": "GND"})
env.step_dict({"action_type": "ADD_COMPONENT", "component_type": "RESISTOR",
"value_idx": 5, "node_a": "VCC", "node_b": "N1"})
env.step_dict({"action_type": "ADD_COMPONENT", "component_type": "NPN_BJT",
"value_idx": 0, "node_a": "N1", "node_b": "N3", "node_c": "GND"})
# ... build full circuit ...
obs, reward, terminated, truncated, info = env.step_dict({"action_type": "FINALIZE"})
print(f"Final reward: {reward:.4f}")
print(f"Frequency: {info['waveform_metrics']['frequency']:.1f} Hz")Tasks
Task Tolerances
Action Space
`gymnasium.spaces.MultiDiscrete([4, 7, 20, 12, 12, 12, 12])` — 7 integers per action.
Invalid action masking is supported via info["action_mask"] (flat boolean array of shape (79,)) compatible with stable-baselines3[contrib] MaskablePPO.
Observation Space
`gymnasium.spaces.Box(shape=(269,), dtype=float32)`
A graph-structured observation (for GNN policies) is available via env.graph_observation().
Reward Function
Range: [0.0, 1.0] (returned by the server and inference.py; internally shaped from [-1, 1])
Full decomposition is logged in info["reward_decomposition"] at every step.
Reference Circuit
The canonical 2-BJT astable multivibrator (9 components):
VCC ─┬── R1(1kΩ) ──┬── C1(15nF) ──┬── R4(47kΩ) ──┐
│ N1 N4 │
│ ├── Q1(C) Q2(B) ──┐ │
└── R2(1kΩ) ──┬── C2(15nF) ──┬── R3(47kΩ) │
N2 N3 │
└── Q2(C) Q1(B) ──┘ │
Q1.E, Q2.E → GND │
Output at N1 (or N2) │f ≈ 1/(1.38·R·C) per half-cycle for symmetric circuit.
Validate the reward function with:
python scripts/evaluate.py --reference --task squarewave-easy --mockTraining with Stable-Baselines3
from stable_baselines3 import PPO
from circuitsynth import CircuitSynthEnv
env = CircuitSynthEnv(task_id="squarewave-easy", mock_sim=True)
model = PPO(
"MlpPolicy", env,
learning_rate=3e-4,
n_steps=2048,
batch_size=64,
verbose=1,
)
model.learn(total_timesteps=500_000)
model.save("circuitsynth_ppo")
# Evaluate
obs, _ = env.reset()
for _ in range(30):
action, _ = model.predict(obs)
obs, r, done, trunc, info = env.step(action)
if done or trunc:
break
print(f"Reward: {r:.4f} Freq: {info['waveform_metrics']['frequency']:.1f} Hz")Curriculum Learning
Start with Easy, then switch to harder tasks:
for task_id in ["squarewave-easy", "squarewave-medium", "squarewave-hard"]:
env = CircuitSynthEnv(task_id=task_id, mock_sim=False)
model = PPO.load("circuitsynth_ppo")
model.set_env(env)
model.learn(total_timesteps=200_000)
model.save(f"circuitsynth_ppo_{task_id}")Scripts
# Random baseline (no ngspice needed)
python scripts/baseline_inference.py --task squarewave-easy --episodes 20 --mock
# Reference circuit reward validation
python scripts/evaluate.py --reference --task squarewave-easy --mock
# Evaluate a trained SB3 model
python scripts/evaluate.py --model circuitsynth_ppo.zip --task squarewave-hard
# Run tests
pytest tests/ -vProject Structure
circuitsynth-squarewave/
├── README.md
├── Dockerfile # HuggingFace Spaces compatible
├── openenv.yaml # OpenEnv specification
├── requirements.txt
├── setup.py
│
├── circuitsynth/
│ ├── __init__.py # Package API
│ ├── env.py # CircuitSynthEnv (main class)
│ ├── tasks.py # Task registry (easy / medium / hard)
│ ├── action_space.py # Typed actions + MultiDiscrete encoding
│ ├── observation.py # Observation builder (flat + graph)
│ ├── reward.py # Decomposed reward function
│ ├── netlist.py # Netlist graph + SPICE serializer + validation
│ ├── simulator.py # ngspice subprocess wrapper + mock fallback
│ ├── waveform.py # Waveform analysis (FFT, DTW, metrics)
│ ├── components.py # Component library + SPICE models
│ └── utils.py # Helpers: seeding, SI formatting, convergence detection
│
├── scripts/
│ ├── baseline_inference.py # Reproducible random policy rollout
│ └── evaluate.py # Offline evaluation + reference circuit
│
└── tests/
├── test_env.py
├── test_reward.py
├── test_simulator.py
└── test_waveform.pyEvaluation Metrics
The environment logs the following metrics per episode (in info dict and env.state()):
MDP Properties
| Testing/Debugging | python -m pytest tests/ |
Baseline Scores
The provided inference.py script serves as a baseline using Qwen/Qwen2.5-72B-Instruct as the LLM agent via the OpenAI client API.
Scores represent partial progress (e.g. correctly wiring the power supply and initial resistors). The agent score is returned on a `[0.0, 1.0]` scale. Generating a structurally perfect transistor-based astable multivibrator is extremely challenging for an LLM baseline zero-shot.
License
MIT License. SPICE device models (2N2222, 1N4148) are public domain.
