Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/inspice/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions examples/inspice/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"rc_lowpass_circuit",
"voltage_divider_netlist",
"subcircuit_rc_stages"
]
92 changes: 92 additions & 0 deletions examples/inspice/rc_lowpass_circuit/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
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

# 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
# |
# [ 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"<pre>{circuit}</pre>"), 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 <code>@</code> 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"<strong>{f_cutoff:,.1f} Hz</strong> "
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)
1 change: 1 addition & 0 deletions examples/inspice/rc_lowpass_circuit/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["inspice", "matplotlib", "numpy"]
41 changes: 41 additions & 0 deletions examples/inspice/rc_lowpass_circuit/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
87 changes: 87 additions & 0 deletions examples/inspice/subcircuit_rc_stages/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ---------------------------------------------------------------------
# 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(
"Real schematics repeat the same pattern many times. InSpice's "
"<code>SubCircuitFactory</code> 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", "input", 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", "input", "n1")
cascade.X("stage2", "rc_stage", "n1", "n2")
cascade.X("stage3", "rc_stage", "n2", "output")

note("The generated netlist shows both the .subckt block and the X-instances:")
display(HTML(f"<pre>{cascade}</pre>"), 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 <code>cascade</code> to a simulator and "
"get the actual simulated response -- the same Python object "
"you built here drives the analysis."
)
1 change: 1 addition & 0 deletions examples/inspice/subcircuit_rc_stages/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["inspice", "matplotlib", "numpy"]
20 changes: 20 additions & 0 deletions examples/inspice/subcircuit_rc_stages/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

72 changes: 72 additions & 0 deletions examples/inspice/voltage_divider_netlist/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ---------------------------------------------------------------------
# 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 "
"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"<pre>{ladder}</pre>"), append=True)

# Walk the components by iterating over the circuit's elements.
note("Iterating over circuit elements:")
rows = ["<table><tr><th>Name</th><th>Nodes</th><th>Value</th></tr>"]
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"<tr><td>{element.name}</td>"
f"<td>{pins}</td>"
f"<td>{value}</td></tr>"
)
rows.append("</table>")
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)
1 change: 1 addition & 0 deletions examples/inspice/voltage_divider_netlist/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["inspice", "matplotlib", "numpy"]
19 changes: 19 additions & 0 deletions examples/inspice/voltage_divider_netlist/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)