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..931a320 --- /dev/null +++ b/examples/inspice/rc_lowpass_circuit/code.py @@ -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"
{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..b4f3ee1 --- /dev/null +++ b/examples/inspice/rc_lowpass_circuit/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/inspice/subcircuit_rc_stages/code.py b/examples/inspice/subcircuit_rc_stages/code.py new file mode 100644 index 0000000..98bfd1b --- /dev/null +++ b/examples/inspice/subcircuit_rc_stages/code.py @@ -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 " + "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", "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"
{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..4c911ec --- /dev/null +++ b/examples/inspice/subcircuit_rc_stages/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/inspice/voltage_divider_netlist/code.py b/examples/inspice/voltage_divider_netlist/code.py new file mode 100644 index 0000000..1a09359 --- /dev/null +++ b/examples/inspice/voltage_divider_netlist/code.py @@ -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"
{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..f77b4ef --- /dev/null +++ b/examples/inspice/voltage_divider_netlist/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True)