From adf2cdfa902d5d154e7af18de65dc9a582a4b146 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:08 +0100 Subject: [PATCH 1/2] Add PyScript examples for inspice Generated by apply_llm_response.py from prompts/inspice/response.toml. Examples included: - rc_lowpass_circuit: Building and inspecting an RC low-pass filter - voltage_divider_netlist: Composing a voltage divider and reading back the netlist - subcircuit_rc_stages: Reusing circuit blocks with SubCircuit Generated-By: apply_llm_response.py --- examples/inspice/README.md | 18 ++++ examples/inspice/order.json | 5 ++ examples/inspice/rc_lowpass_circuit/code.py | 84 +++++++++++++++++++ .../inspice/rc_lowpass_circuit/config.toml | 1 + examples/inspice/rc_lowpass_circuit/setup.py | 49 +++++++++++ examples/inspice/subcircuit_rc_stages/code.py | 81 ++++++++++++++++++ .../inspice/subcircuit_rc_stages/config.toml | 1 + .../inspice/subcircuit_rc_stages/setup.py | 26 ++++++ .../inspice/voltage_divider_netlist/code.py | 65 ++++++++++++++ .../voltage_divider_netlist/config.toml | 1 + .../inspice/voltage_divider_netlist/setup.py | 26 ++++++ 11 files changed, 357 insertions(+) create mode 100644 examples/inspice/README.md create mode 100644 examples/inspice/order.json create mode 100644 examples/inspice/rc_lowpass_circuit/code.py create mode 100644 examples/inspice/rc_lowpass_circuit/config.toml create mode 100644 examples/inspice/rc_lowpass_circuit/setup.py create mode 100644 examples/inspice/subcircuit_rc_stages/code.py create mode 100644 examples/inspice/subcircuit_rc_stages/config.toml create mode 100644 examples/inspice/subcircuit_rc_stages/setup.py create mode 100644 examples/inspice/voltage_divider_netlist/code.py create mode 100644 examples/inspice/voltage_divider_netlist/config.toml create mode 100644 examples/inspice/voltage_divider_netlist/setup.py diff --git a/examples/inspice/README.md b/examples/inspice/README.md new file mode 100644 index 0000000..ad8adc2 --- /dev/null +++ b/examples/inspice/README.md @@ -0,0 +1,18 @@ +# inspice Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/inspice/order.json b/examples/inspice/order.json new file mode 100644 index 0000000..2ba8fe4 --- /dev/null +++ b/examples/inspice/order.json @@ -0,0 +1,5 @@ +[ + "rc_lowpass_circuit", + "voltage_divider_netlist", + "subcircuit_rc_stages" +] diff --git a/examples/inspice/rc_lowpass_circuit/code.py b/examples/inspice/rc_lowpass_circuit/code.py new file mode 100644 index 0000000..f63bb73 --- /dev/null +++ b/examples/inspice/rc_lowpass_circuit/code.py @@ -0,0 +1,84 @@ +""" +A first look at InSpice: describing an electronic circuit in Python. + +InSpice (a fork of PySpice) lets you build SPICE netlists from Python +objects, then hand them off to a simulator like Ngspice or Xyce. +In this first example we'll just build a circuit and inspect the +netlist InSpice generates for us. + +See https://github.com/Innovoltive/InSpice for documentation. +""" +from IPython.core.display import display, HTML + +# A classic RC low-pass filter: +# +# Vin o---[ R1 ]---+---o Vout +# | +# [ C1 ] +# | +# GND +# +# The cutoff frequency is f_c = 1 / (2 * pi * R * C). + +heading("1. Describing an RC low-pass filter") +note( + "We give the circuit a name, then add components by calling " + "methods on it. The single-letter method picks the SPICE " + "device type: R for resistor, C for capacitor, V for voltage " + "source. Nodes are just strings -- here 'input', 'output', " + "and the built-in ground reference." +) + +circuit = Circuit("RC low-pass filter") + +# A 1 V AC source between the 'input' node and ground. +circuit.SinusoidalVoltageSource( + "input", "input", circuit.gnd, + amplitude=1 @ u_V, frequency=1 @ u_kHz, +) +# Resistor R1 between 'input' and 'output'. +circuit.R(1, "input", "output", 1 @ u_kOhm) +# Capacitor C1 between 'output' and ground. +circuit.C(1, "output", circuit.gnd, 1 @ u_uF) + +note("InSpice rendered our circuit as the following SPICE netlist:") +display(HTML(f"
{circuit}
"), append=True) + +# Component values are first-class Python objects with units. +r1 = circuit["R1"] +c1 = circuit["C1"] +note( + f"R1 = {r1.resistance}, C1 = {c1.capacitance}. " + "The @ operator attaches a unit, and the " + "resulting quantity behaves like a number in arithmetic." +) + +# Compute the theoretical cutoff frequency from the component values. +r_value = float(r1.resistance) # ohms +c_value = float(c1.capacitance) # farads +f_cutoff = 1.0 / (2.0 * np.pi * r_value * c_value) + +note( + f"Theoretical cutoff frequency: " + f"{f_cutoff:,.1f} Hz " + f"({f_cutoff / 1000:.2f} kHz)." +) + +# Plot the analytic magnitude response |H(f)| = 1 / sqrt(1 + (f/f_c)^2). +frequencies = np.logspace(1, 6, 400) +magnitude_db = 20 * np.log10( + 1.0 / np.sqrt(1.0 + (frequencies / f_cutoff) ** 2) +) + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.semilogx(frequencies, magnitude_db, color="steelblue", linewidth=2) +ax.axvline(f_cutoff, color="darkorange", linestyle="--", + label=f"f_c = {f_cutoff:,.0f} Hz") +ax.axhline(-3, color="gray", linestyle=":", label="-3 dB") +ax.set_title("RC low-pass filter: analytic frequency response") +ax.set_xlabel("Frequency (Hz)") +ax.set_ylabel("Gain (dB)") +ax.grid(True, which="both", alpha=0.3) +ax.legend() +fig.tight_layout() +display(fig, append=True) diff --git a/examples/inspice/rc_lowpass_circuit/config.toml b/examples/inspice/rc_lowpass_circuit/config.toml new file mode 100644 index 0000000..143d714 --- /dev/null +++ b/examples/inspice/rc_lowpass_circuit/config.toml @@ -0,0 +1 @@ +packages = ["inspice", "matplotlib", "numpy"] diff --git a/examples/inspice/rc_lowpass_circuit/setup.py b/examples/inspice/rc_lowpass_circuit/setup.py new file mode 100644 index 0000000..375accc --- /dev/null +++ b/examples/inspice/rc_lowpass_circuit/setup.py @@ -0,0 +1,49 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +# Package imports for this example. +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms diff --git a/examples/inspice/subcircuit_rc_stages/code.py b/examples/inspice/subcircuit_rc_stages/code.py new file mode 100644 index 0000000..8869c14 --- /dev/null +++ b/examples/inspice/subcircuit_rc_stages/code.py @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------- +# Defining a reusable RC stage as a SubCircuit and cascading copies. +# --------------------------------------------------------------------- + +heading("3. Cascading RC stages with SubCircuit") +note( + "Real schematics repeat the same pattern many times. InSpice's " + "SubCircuitFactory lets you define a block once, " + "give it named external pins, and then drop instances of it " + "into a parent circuit. Here we make a one-pole RC stage and " + "cascade three of them to build a third-order filter." +) + + +class RCStage(SubCircuitFactory): + """A single RC low-pass stage with input and output pins.""" + + NAME = "rc_stage" + NODES = ("input", "output") + + def __init__(self, resistance=1 @ u_kOhm, capacitance=100 * u_uF(0.001)): + super().__init__() + # Resistor between the external 'input' and 'output' pins. + self.R(1, "input", "output", resistance) + # Capacitor from 'output' to the subcircuit's local ground. + self.C(1, "output", self.gnd, capacitance) + + +cascade = Circuit("Three-stage RC cascade") +cascade.SinusoidalVoltageSource( + "src", "in", cascade.gnd, + amplitude=1 @ u_V, frequency=1 @ u_kHz, +) +# Register the subcircuit definition once... +cascade.subcircuit(RCStage(resistance=1 @ u_kOhm, + capacitance=100 * u_uF(0.001))) + +# ...then instantiate it three times, wiring stage outputs to the +# next stage's input. +cascade.X("stage1", "rc_stage", "in", "n1") +cascade.X("stage2", "rc_stage", "n1", "n2") +cascade.X("stage3", "rc_stage", "n2", "out") + +note("The generated netlist shows both the .subckt block and the X-instances:") +display(HTML(f"
{cascade}
"), append=True) + +# Each identical RC stage multiplies the transfer function, so the +# cascade has a steeper roll-off than a single stage. We can compare +# the analytic magnitude responses side-by-side. +r_value = 1_000.0 # 1 kOhm +c_value = 0.1e-6 # 0.1 uF +f_cutoff = 1.0 / (2.0 * np.pi * r_value * c_value) + +frequencies = np.logspace(1, 7, 400) +single_stage = 1.0 / np.sqrt(1.0 + (frequencies / f_cutoff) ** 2) +three_stage = single_stage ** 3 + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.semilogx(frequencies, 20 * np.log10(single_stage), + color="steelblue", linewidth=2, label="1 RC stage") +ax.semilogx(frequencies, 20 * np.log10(three_stage), + color="crimson", linewidth=2, label="3 cascaded stages") +ax.axvline(f_cutoff, color="gray", linestyle="--", + label=f"f_c = {f_cutoff:,.0f} Hz") +ax.axhline(-3, color="gray", linestyle=":", alpha=0.6) +ax.set_title("Single stage vs. three-stage cascade") +ax.set_xlabel("Frequency (Hz)") +ax.set_ylabel("Gain (dB)") +ax.set_ylim(-120, 5) +ax.grid(True, which="both", alpha=0.3) +ax.legend() +fig.tight_layout() +display(fig, append=True) + +note( + "Notice the cascade rolls off three times faster (about 60 dB " + "per decade) past the cutoff. With a Ngspice shared library " + "available, you'd hand cascade to a simulator and " + "get the actual simulated response -- the same Python object " + "you built here drives the analysis." +) diff --git a/examples/inspice/subcircuit_rc_stages/config.toml b/examples/inspice/subcircuit_rc_stages/config.toml new file mode 100644 index 0000000..143d714 --- /dev/null +++ b/examples/inspice/subcircuit_rc_stages/config.toml @@ -0,0 +1 @@ +packages = ["inspice", "matplotlib", "numpy"] diff --git a/examples/inspice/subcircuit_rc_stages/setup.py b/examples/inspice/subcircuit_rc_stages/setup.py new file mode 100644 index 0000000..db862b3 --- /dev/null +++ b/examples/inspice/subcircuit_rc_stages/setup.py @@ -0,0 +1,26 @@ +"""Setup for the subcircuit example. No IPython shim needed here.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit, SubCircuit, SubCircuitFactory +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms diff --git a/examples/inspice/voltage_divider_netlist/code.py b/examples/inspice/voltage_divider_netlist/code.py new file mode 100644 index 0000000..84cb166 --- /dev/null +++ b/examples/inspice/voltage_divider_netlist/code.py @@ -0,0 +1,65 @@ +# --------------------------------------------------------------------- +# Building a resistor ladder and computing node voltages by hand. +# --------------------------------------------------------------------- + +heading("2. A four-resistor voltage divider") +note( + "InSpice circuits compose nicely with ordinary Python: we can " + "use a loop to chain resistors between numbered nodes. Here we " + "build a ladder of four 1 kΩ resistors driven by a 12 V supply, " + "then read the netlist back to confirm the topology." +) + +ladder = Circuit("Four-resistor voltage divider") +ladder.V("supply", "n0", ladder.gnd, 12 @ u_V) + +# Chain resistors n0 -> n1 -> n2 -> n3 -> ground. +nodes = ["n0", "n1", "n2", "n3", ladder.gnd] +for index in range(4): + ladder.R(index + 1, nodes[index], nodes[index + 1], 1 @ u_kOhm) + +note("Generated SPICE netlist:") +display(HTML(f"
{ladder}
"), append=True) + +# Walk the components by iterating over the circuit's elements. +note("Iterating over circuit elements:") +rows = [""] +for element in ladder.elements: + pins = ", ".join(str(pin) for pin in element.nodes) + value = getattr(element, "resistance", + getattr(element, "dc_value", "")) + rows.append( + f"" + f"" + f"" + ) +rows.append("
NameNodesValue
{element.name}{pins}{value}
") +display(HTML("".join(rows)), append=True) + +# For a chain of equal resistors the voltage at each tap is just a +# linear interpolation between the supply and ground. +supply_voltage = 12.0 +n_resistors = 4 +tap_voltages = [ + supply_voltage * (n_resistors - i) / n_resistors + for i in range(n_resistors + 1) +] + +note( + "With four equal resistors, each one drops a quarter of the " + "supply, giving the following expected node voltages:" +) +fig, ax = plt.subplots(figsize=(7, 4)) +ax.plot(range(len(tap_voltages)), tap_voltages, + marker="o", color="crimson", linewidth=2) +for i, v in enumerate(tap_voltages): + ax.annotate(f"{v:.2f} V", (i, v), + textcoords="offset points", xytext=(8, 6)) +ax.set_xticks(range(len(tap_voltages))) +ax.set_xticklabels([f"n{i}" if i < 4 else "GND" + for i in range(len(tap_voltages))]) +ax.set_ylabel("Voltage (V)") +ax.set_title("Voltage at each tap of the divider") +ax.grid(True, alpha=0.3) +fig.tight_layout() +display(fig, append=True) diff --git a/examples/inspice/voltage_divider_netlist/config.toml b/examples/inspice/voltage_divider_netlist/config.toml new file mode 100644 index 0000000..143d714 --- /dev/null +++ b/examples/inspice/voltage_divider_netlist/config.toml @@ -0,0 +1 @@ +packages = ["inspice", "matplotlib", "numpy"] diff --git a/examples/inspice/voltage_divider_netlist/setup.py b/examples/inspice/voltage_divider_netlist/setup.py new file mode 100644 index 0000000..b6e0fda --- /dev/null +++ b/examples/inspice/voltage_divider_netlist/setup.py @@ -0,0 +1,26 @@ +"""Setup for the voltage divider example. No IPython shim needed here.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms From 3419423f7ab22eee4aec5e336db908afed653c3f Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Jun 2026 15:18:54 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/inspice/rc_lowpass_circuit/code.py | 8 ++++++++ examples/inspice/rc_lowpass_circuit/setup.py | 8 -------- examples/inspice/subcircuit_rc_stages/code.py | 12 +++++++++--- examples/inspice/subcircuit_rc_stages/setup.py | 6 ------ examples/inspice/voltage_divider_netlist/code.py | 7 +++++++ examples/inspice/voltage_divider_netlist/setup.py | 7 ------- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/inspice/rc_lowpass_circuit/code.py b/examples/inspice/rc_lowpass_circuit/code.py index f63bb73..931a320 100644 --- a/examples/inspice/rc_lowpass_circuit/code.py +++ b/examples/inspice/rc_lowpass_circuit/code.py @@ -10,6 +10,14 @@ """ from IPython.core.display import display, HTML +# Package imports for this example. +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms + + # A classic RC low-pass filter: # # Vin o---[ R1 ]---+---o Vout diff --git a/examples/inspice/rc_lowpass_circuit/setup.py b/examples/inspice/rc_lowpass_circuit/setup.py index 375accc..b4f3ee1 100644 --- a/examples/inspice/rc_lowpass_circuit/setup.py +++ b/examples/inspice/rc_lowpass_circuit/setup.py @@ -39,11 +39,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -# Package imports for this example. -import numpy as np -import matplotlib.pyplot as plt -import InSpice -from InSpice import Circuit -from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms diff --git a/examples/inspice/subcircuit_rc_stages/code.py b/examples/inspice/subcircuit_rc_stages/code.py index 8869c14..98bfd1b 100644 --- a/examples/inspice/subcircuit_rc_stages/code.py +++ b/examples/inspice/subcircuit_rc_stages/code.py @@ -1,6 +1,12 @@ # --------------------------------------------------------------------- # Defining a reusable RC stage as a SubCircuit and cascading copies. # --------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit, SubCircuit, SubCircuitFactory +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms + heading("3. Cascading RC stages with SubCircuit") note( @@ -28,7 +34,7 @@ def __init__(self, resistance=1 @ u_kOhm, capacitance=100 * u_uF(0.001)): cascade = Circuit("Three-stage RC cascade") cascade.SinusoidalVoltageSource( - "src", "in", cascade.gnd, + "src", "input", cascade.gnd, amplitude=1 @ u_V, frequency=1 @ u_kHz, ) # Register the subcircuit definition once... @@ -37,9 +43,9 @@ def __init__(self, resistance=1 @ u_kOhm, capacitance=100 * u_uF(0.001)): # ...then instantiate it three times, wiring stage outputs to the # next stage's input. -cascade.X("stage1", "rc_stage", "in", "n1") +cascade.X("stage1", "rc_stage", "input", "n1") cascade.X("stage2", "rc_stage", "n1", "n2") -cascade.X("stage3", "rc_stage", "n2", "out") +cascade.X("stage3", "rc_stage", "n2", "output") note("The generated netlist shows both the .subckt block and the X-instances:") display(HTML(f"
{cascade}
"), append=True) diff --git a/examples/inspice/subcircuit_rc_stages/setup.py b/examples/inspice/subcircuit_rc_stages/setup.py index db862b3..4c911ec 100644 --- a/examples/inspice/subcircuit_rc_stages/setup.py +++ b/examples/inspice/subcircuit_rc_stages/setup.py @@ -18,9 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import numpy as np -import matplotlib.pyplot as plt -import InSpice -from InSpice import Circuit, SubCircuit, SubCircuitFactory -from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms diff --git a/examples/inspice/voltage_divider_netlist/code.py b/examples/inspice/voltage_divider_netlist/code.py index 84cb166..1a09359 100644 --- a/examples/inspice/voltage_divider_netlist/code.py +++ b/examples/inspice/voltage_divider_netlist/code.py @@ -2,6 +2,13 @@ # Building a resistor ladder and computing node voltages by hand. # --------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import InSpice +from InSpice import Circuit +from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms + + heading("2. A four-resistor voltage divider") note( "InSpice circuits compose nicely with ordinary Python: we can " diff --git a/examples/inspice/voltage_divider_netlist/setup.py b/examples/inspice/voltage_divider_netlist/setup.py index b6e0fda..f77b4ef 100644 --- a/examples/inspice/voltage_divider_netlist/setup.py +++ b/examples/inspice/voltage_divider_netlist/setup.py @@ -17,10 +17,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import numpy as np -import matplotlib.pyplot as plt -import InSpice -from InSpice import Circuit -from InSpice.Unit import u_V, u_Hz, u_Ohm, u_uF, u_kHz, u_kOhm, u_ms