Skip to content

706 - Python Integration

Automate GeckoCIRCUITS simulations from Python and integrate with the scientific Python ecosystem. Multiple integration methods support different use cases—from simple subprocess automation to high-performance API communication.

Overview

Python integration enables:

  • Subprocess Control - Run GeckoCIRCUITS from Python scripts, parse output
  • Parameter Sweeps - Use NumPy/SciPy for design space exploration
  • Data Analysis - Pandas, NumPy, Matplotlib for post-processing
  • Optimization - SciPy.optimize for automated design optimization
  • REST API (planned) - Future high-performance server-based integration

Real-World Example

See resources/validate_circuits.py for a production example that runs batch circuit simulations and validates results.

Integration Methods

Run GeckoCIRCUITS as an external process and communicate via files or stdout:

import subprocess
import json
import tempfile

def run_simulation(circuit_file, parameters):
    """Run GeckoCIRCUITS simulation from Python."""

    # Build command with parameters
    cmd = [
        'java', '-Xmx3G', '-jar', 'gecko-1.0-jar-with-dependencies.jar',
        '--headless',
        '--circuit', circuit_file,
        '--output', 'results.json'
    ]

    # Add parameters
    for name, value in parameters.items():
        cmd.extend(['--param', f'{name}={value}'])

    # Run simulation
    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        raise RuntimeError(f"Simulation failed: {result.stderr}")

    # Parse results
    with open('results.json') as f:
        return json.load(f)

# Usage
results = run_simulation('buck_converter.ipes', {
    'R_load': 10.0,
    'PWM1.frequency': 100e3
})

Headless Mode

Use --headless flag to run without GUI overhead, or --remote mode for persistent sessions across multiple simulations.

Method 2: File-Based Parameter Modification

Directly modify circuit .ipes files (gzip-compressed XML):

import zipfile
import xml.etree.ElementTree as ET
import gzip
import shutil

def modify_circuit_param(ipes_file, component, param, value):
    """Modify parameter in .ipes file and save as new circuit."""

    # Extract gzip
    with gzip.open(ipes_file, 'rb') as f_in:
        xml_content = f_in.read()

    # Parse XML
    root = ET.fromstring(xml_content)

    # Find and modify component parameter
    for comp in root.findall('.//component[@name="' + component + '"]'):
        param_elem = comp.find(f".//parameter[@name='{param}']")
        if param_elem is not None:
            param_elem.set('value', str(value))

    # Save modified circuit
    output_file = f'modified_{ipes_file}'
    new_xml = ET.tostring(root)

    with gzip.open(output_file, 'wb') as f_out:
        f_out.write(new_xml)

    return output_file

Method 3: REST API (Future)

Planned high-performance integration via Spring Boot REST server:

import requests

# Future API endpoint
BASE_URL = "http://localhost:8080/api"

response = requests.post(f"{BASE_URL}/simulation/run", json={
    "circuit": "buck_converter.ipes",
    "parameters": {"R_load": 10.0},
    "duration": 0.01,
    "timestep": 1e-7
})

results = response.json()

REST API Status

The REST API is under development in the gecko-rest-api module. Currently, use subprocess or file-based methods.

Python Wrapper Class

import requests
import numpy as np
import pandas as pd

class GeckoCIRCUITS:
    def __init__(self, base_url="http://localhost:8080/api"):
        self.base_url = base_url

    def load_circuit(self, path):
        """Load circuit file."""
        response = requests.post(
            f"{self.base_url}/circuit/load",
            json={"path": path}
        )
        return response.json()

    def set_parameter(self, name, value):
        """Set component parameter."""
        requests.post(
            f"{self.base_url}/circuit/parameter",
            json={"name": name, "value": value}
        )

    def run_simulation(self, duration, timestep=1e-7):
        """Run simulation and wait for completion."""
        requests.post(
            f"{self.base_url}/simulation/run",
            json={"duration": duration, "timestep": timestep}
        )

    def get_measurement(self, scope, channel, measurement):
        """Get scope measurement (mean, rms, max, min, pp)."""
        response = requests.get(
            f"{self.base_url}/simulation/measurement",
            params={"scope": scope, "channel": channel, "type": measurement}
        )
        return response.json()["value"]

    def get_waveform(self, scope, channel):
        """Get waveform data as numpy arrays."""
        response = requests.get(
            f"{self.base_url}/simulation/waveform",
            params={"scope": scope, "channel": channel}
        )
        data = response.json()
        return np.array(data["time"]), np.array(data["values"])

Parameter Sweeps

Single Sweep

import matplotlib.pyplot as plt

gecko = GeckoCIRCUITS()
gecko.load_circuit("buck_converter.ipes")

duties = np.linspace(0.2, 0.8, 13)
vout = []
efficiency = []

for d in duties:
    gecko.set_parameter("PWM1.dutyCycle", d)
    gecko.run_simulation(0.01)

    vout.append(gecko.get_measurement("SCOPE", "Vout", "mean"))
    pin = gecko.get_measurement("SCOPE", "Pin", "mean")
    pout = gecko.get_measurement("SCOPE", "Pout", "mean")
    efficiency.append(pout / pin * 100)

# Plot results
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

ax1.plot(duties, vout, 'b-o')
ax1.set_xlabel('Duty Cycle')
ax1.set_ylabel('Output Voltage (V)')
ax1.grid(True)

ax2.plot(duties, efficiency, 'r-o')
ax2.set_xlabel('Duty Cycle')
ax2.set_ylabel('Efficiency (%)')
ax2.grid(True)

plt.tight_layout()
plt.savefig('sweep_results.png')

Multi-Parameter Sweep

from itertools import product
import pandas as pd

# Parameter ranges
fsw_values = [50e3, 100e3, 200e3]
L_values = [22e-6, 47e-6, 100e-6]
load_values = [5, 10, 20]

results = []

for fsw, L, load in product(fsw_values, L_values, load_values):
    gecko.set_parameter("PWM1.frequency", fsw)
    gecko.set_parameter("L1.inductance", L)
    gecko.set_parameter("R_load", load)

    gecko.run_simulation(0.01)

    results.append({
        'fsw': fsw,
        'L': L,
        'load': load,
        'vout': gecko.get_measurement("SCOPE", "Vout", "mean"),
        'ripple': gecko.get_measurement("SCOPE", "IL", "pp"),
        'efficiency': gecko.get_measurement("SCOPE", "efficiency", "mean")
    })

# Create DataFrame
df = pd.DataFrame(results)
df.to_csv('parameter_sweep.csv', index=False)

# Analyze results
print(df.groupby('fsw')['efficiency'].mean())

Optimization

SciPy Optimization

from scipy.optimize import minimize

def efficiency_objective(params):
    """Objective function to maximize efficiency."""
    L, C, fsw = params

    gecko.set_parameter("L1.inductance", L)
    gecko.set_parameter("C1.capacitance", C)
    gecko.set_parameter("PWM1.frequency", fsw)

    gecko.run_simulation(0.01)

    efficiency = gecko.get_measurement("SCOPE", "efficiency", "mean")

    # Minimize negative efficiency (maximize efficiency)
    return -efficiency

# Initial guess
x0 = [47e-6, 100e-6, 100e3]

# Bounds
bounds = [
    (10e-6, 200e-6),   # L: 10-200 µH
    (22e-6, 470e-6),   # C: 22-470 µF
    (50e3, 500e3)      # fsw: 50-500 kHz
]

# Optimize
result = minimize(efficiency_objective, x0, bounds=bounds, method='L-BFGS-B')

print(f"Optimal L: {result.x[0]*1e6:.1f} µH")
print(f"Optimal C: {result.x[1]*1e6:.1f} µF")
print(f"Optimal fsw: {result.x[2]/1e3:.0f} kHz")
print(f"Max efficiency: {-result.fun*100:.2f}%")

Data Analysis

FFT Analysis

from scipy.fft import fft, fftfreq

time, voltage = gecko.get_waveform("SCOPE", "Vout")

# Compute FFT
N = len(time)
dt = time[1] - time[0]
yf = fft(voltage)
xf = fftfreq(N, dt)

# Plot spectrum
plt.figure(figsize=(10, 6))
plt.semilogy(xf[:N//2]/1e3, np.abs(yf[:N//2]))
plt.xlabel('Frequency (kHz)')
plt.ylabel('Magnitude')
plt.title('Output Voltage Spectrum')
plt.grid(True)
plt.savefig('spectrum.png')

Real-World Example: validate_circuits.py

The GeckoCIRCUITS repository includes a complete example of Python integration:

# From: resources/validate_circuits.py
# Validates all .ipes circuit files in a directory

import subprocess
import glob
from pathlib import Path

def validate_circuit(circuit_file):
    """Run simulation and check for errors."""
    result = subprocess.run([
        'java', '-jar', 'gecko.jar',
        '--headless',
        '--circuit', circuit_file,
        '--validate'
    ], capture_output=True, text=True)

    return result.returncode == 0

# Validate all circuits
circuits = glob.glob('resources/examples/**/*.ipes', recursive=True)
passed = sum(1 for c in circuits if validate_circuit(c))

print(f"Passed: {passed}/{len(circuits)}")

This script demonstrates: - Subprocess invocation with multiple options - Batch processing of multiple files - Error detection and reporting - Integration with standard Python tools

Best Practices

Best Practice Why Example
Use subprocess for isolation Avoid JVM overhead in Python One JAR invocation per simulation
Cache heavy simulations Avoid redundant computation Store results in SQLite or HDF5
Batch parameters efficiently Reduce process startup overhead Group related parameter sweeps
Use parallel processing Exploit multiple CPU cores ProcessPoolExecutor for independent sweeps
# Parallel sweeps with ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import numpy as np

def sweep_single_value(load_value):
    """Run single simulation."""
    return run_simulation('buck.ipes', {'R_load': load_value})

# Parallel execution
with ProcessPoolExecutor(max_workers=4) as executor:
    loads = np.linspace(5, 50, 10)
    results = list(executor.map(sweep_single_value, loads))

Resources

  • Tutorial directory: resources/tutorials/7xx_scripting_automation/706_python_integration/
  • Validation script: resources/validate_circuits.py
  • Example circuits: resources/examples/