Circuit Simulator allows you to create a Draw.io circuit diagram or define a circuit programatically, then simulate it. Circuit Simulator's library of electrical and Draw.io-supported components is extensible, allowing you to easily define custom components.
Below is an example of a Draw.io circuit diagram that can be simulated.
Alternatively, the circuit can be defined in a script as follows, which specifies the circuit's nodes and components, and configures the simulation. When a Draw.io circuit diagram is simulated, it is converted to this format under the hood.
from base import Node, Terminal
from component_library import (
Capacitor,
AcVoltageSource,
FullBridgeRectifier,
Resistor,
)
from simulation import LivePlottedSimulation
positive_in = Node("positive_in")
negative_in = Node("negative_in", is_ground=True)
positive_out_1 = Node("positive_out_1")
positive_out_2 = Node("positive_out_2")
negative_out = Node("negative_out")
sim = LivePlottedSimulation(
components=[
V_s := AcVoltageSource(
"V_s",
AcVoltageSource.Terminae[Terminal](
negative=Terminal(negative_in), positive=Terminal(positive_in)
),
peak_voltage=10,
frequency_hz=60,
),
rectifier := FullBridgeRectifier(
"rectifier",
FullBridgeRectifier.Terminae[Terminal](
Terminal(positive_in),
Terminal(negative_in),
Terminal(positive_out_1),
Terminal(negative_out),
),
diode_params=dict(
saturation_current=1e-3,
thermal_voltage=0.1,
ideality_factor=1,
),
),
R_1 := Resistor(
"R_1",
Resistor.Terminae[Terminal](
negative=Terminal(positive_out_1), positive=Terminal(positive_out_2)
),
resistance=0.1,
),
C := Capacitor(
"C",
Capacitor.Terminae[Terminal](
negative=Terminal(negative_out), positive=Terminal(positive_out_2)
),
capacitance=5e-4,
),
R_2 := Resistor(
"R_2",
Resistor.Terminae[Terminal](
negative=Terminal(negative_out), positive=Terminal(positive_out_2)
),
resistance=50,
),
],
time_step_s=1e-4,
plot_signals={
"in": rectifier.input_voltage,
"out_1": rectifier.output_voltage,
"out_2": R_2.voltage_difference,
},
yaxis_limits=(-10, 10),
)
sim.run()Simulating the Draw.io circuit using the CLI (or running the equivalent script) produces the following output. Using the LivePlottedSimulation class allows the specified signals to be shown over time in an animated plot:
A circuit is modeled as a network of components and nodes.
A node, which is an interconnection between two or more components and may represent one or more wires, has one voltage. A node is connected to components by the components' terminals. Following the node-voltage method (conservation of currents), the currents flowing from each node to its neighboring components via their terminals must sum to zero. This equation is each node's contribution to the system of equations that models the circuit; a circuit's nodes provide n unknown voltages and n equations.
A component has one or more terminals by which it connects to other components via nodes. The currents flowing into each component from its neighboring nodes are unknowns and must sum to zero, which is one of the equations contributed by each component (similar to that of each node). Components contribute additional (often nonlinear) equations; as a basic example, the resistor component contributes Ohm's law in addition to its conservation of currents.
The simulation works by solving the nonlinear system of equations for the unknown node voltages and terminal currents in each time step. Because the system may be overdetermined, a least-squares solver (from MINPACK) is used (although the least-squares error should always be minimized to zero).
Is the world "terminae" made up? In English, yes. Is it singular or plural? Also yes.
Terminae is the name of a dataclass defined under each electrical component, in which each of the dataclass's fields refers to one of the component's terminals (each field's name is the name of the terminal).
But using a plural name like Terminals would be weird for a class (it would cause confusion with the Terminal class and lists thereof) and is thus avoided.
Note: When we say that a Terminae class is "defined under each electrical component", we mean that literally -- a Terminae class is defined in the body of each component class, e.g. Resistor, so you we can write Resistor.Terminae. This is unusual but not weird and a way to make the circuit simulator plug-and-play with any valid component.
Note: The Terminae classes are actually generics, so while they usually hold Terminal instances (i.e., Resistor.Terminae[Terminal](positive=Terminal(..., they can also hold other types for each terminal (in particular, the terminal's position on a Draw.io object). This is another reason not to call the Terminae class something like Terminals.
component_library.py
Every component is a subclass of BaseComponent, thus implementing a subclass of BaseTerminae and the _equations abstractmethod.
Many components have only two terminals, one negative and one positive. Even if the component is non-polarized, it makes sense to label its terminals like this for distinction and to establish a sign convention. Two-terminal components also have an obvious voltage value (measured across the two terminals) and an obvious current value (from the positive terminal to the negative one), which ought to be convenient to access without needing the same code over and over. For these reasons, a TwoTerminalBaseComponent abstract subclass of BaseComponent provides these nicities and is inherited by many components.
Many components are an assembly of sub- components (e.g., a rectifier assembled from diodes) or are modeled by sub-components (e.g., a diode modeled by a resistor in series with an ideal diode). These 'super components' have attributes of both circuits and regular components.
When defining a circuit programatically, the nodes are defined first, and then the components are defined, specifying the node to which each terminal of each component connects. Not only does this support IDE features like autocomplete when specifying all of a component's terminals via it's Terminae class, but it also encourages creating representations of the circuit's topology that are efficient in terms of having the fewest nodes. In contrast, defining the components first and connecting their terminals after would be prone to accidentally leaving a terminal unconnected. While this would be checked at runtime, being confident even just writing the script is better.
The Circuit Simulator supports circuits drawn in Draw.io consisting of electrical objects whose terminals (the Draw.io objects' anchor points) are joined by wires (Draw.io connectors). Each electrical object must be joined to a textbox containing the electrical object's configuration in YAML format. Furthermore, the diagram must include a textbox containing the circuit simulation's configuration, again in YAML format. Each page/tab in a Draw.io file can contain a different circuit / circuit simulation. The Draw.io file can contain other pages, and a page can contain other objects than those defining the circuit as long as they do not create ambiguity when the Circuit Simulator is converting the diagram.
Again, when defining a circuit programatically, the nodes are defined first and components are then placed between those nodes. However, when drawing a circuit diagram in Draw.io, not only must the components be placed first and their terminals subsequently joined by wires, but these wires do not generally represent nodes and they are not generally the most efficient way to represent the circuit's topology as they can only connect two components at a time. This is solved by realizing that wires are just a specific case of a more general entity called a net, and that these wires can be collapsed into a more efficient, smaller quantity of nets that can then be treated as nodes.