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) 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) + 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 = ["| Name | Nodes | Value |
|---|---|---|
| {element.name} | " + f"{pins} | " + f"{value} |
{text}
"), append=True)