A TypeScript library for simulating logic circuits. Build complex digital systems from basic logic gates, manage signal propagation, and create reusable composite elements.
circuit-ts is designed for:
- Education: Learning how digital logic gates and complex circuits (like flip-flops, adders, etc.) work through "code as a circuit".
- Digital Simulation: Prototyping and simulating digital systems in a software environment.
- Circuit-as-Code: Building reusable logic components using a developer-friendly API.
Analogues:
- Logisim / Digital: Graphical tools for circuit simulation.
circuit-tsprovides a similar experience but in a purely programmatic way. - Hardware Description Languages (VHDL/Verilog): While much simpler,
circuit-tsshares the concept of defining logic and connections, but runs in a standard JavaScript/TypeScript environment.
- Generic Value Support: While primarily used for boolean logic, the core architecture supports any value type (numbers, strings, objects).
- Signal Propagation: Advanced signal propagation mechanism with infinite loop detection and randomization to avoid deterministic bias.
- Dirty State Management: Efficient updates by only propagating changed signals.
- Composite Elements: Easily wrap complex circuits into single reusable components.
- Connection Management: Simplified API for connecting and disconnecting elements.
- Extensible: Create your own elements and propagators by extending base classes.
Ports are the ports of an Element.
- InputPort: Receives values from other elements.
- OutputPort: Sends values to connected inputs.
Elements are the building blocks of your circuit.
- BaseElement: The foundation for all logic gates.
- UniformElement: The foundation for all non-composite logic gates.
- BusElement: A special element that passes signals through without logic changes, useful for routing.
- CompositeElement: Encapsulates a group of connected elements into a single unit with its own inputs and outputs.
Propagators manage how signals move through the circuit.
- SignalPropagator: Handles the flow of signals across connections. It uses a queue-based approach to propagate values from outputs to inputs.
- ResetElementPropagator: Used to initialize or reset the state of an entire circuit by marking all ports as "dirty".
Handles the wiring between output and input ports, ensuring validity and preventing duplicate connections.
A key concept in circuit-ts is the dirty state. A port is considered "dirty" if:
- Its value has changed.
- It has been explicitly marked as dirty (e.g., during initialization or reset).
The SignalPropagator only processes ports that are dirty. When an element's input becomes dirty, the element recalculates its logic and, if its output value changes, that output also becomes dirty, continuing the propagation chain.
import * as circuit from 'circuit-ts';
// 1. Setup environment
const connectionManager = circuit.boolean.factories.createConnectionManager();
const signalPropagator = circuit.boolean.factories.createSignalPropagator();
// 2. Create elements
const andElement = circuit.boolean.factories.createAndElement(2); // 2-input AND element
const notElement = circuit.boolean.factories.createNotElement();
// 3. Connect them (AND output -> NOT input)
connectionManager.connect(andElement.outputs[0], notElement.inputs[0]);
// 4. Initialize elements (marks all outputs as dirty and performs initial propagation)
andElement.init();
notElement.init();
// 5. Set values and propagate
andElement.inputs[0].value = true;
andElement.inputs[1].value = true;
// Propagate starting from the changed inputs
signalPropagator.propagate(andElement.inputs);
console.log(notElement.outputs[0].value); // false (AND(true, true) = true, NOT(true) = false)You can build your own complex elements by connecting basic gates and wrapping them into a CompositeElement.
import * as circuit from 'circuit-ts';
// 1. Setup environment
const connectionManager = circuit.boolean.factories.createConnectionManager();
const signalPropagator = circuit.boolean.factories.createSignalPropagator();
const resetPropagator = circuit.boolean.factories.createResetElementPropagator();
const factory = new circuit.boolean.factories.CompositeElementFactory(
connectionManager,
signalPropagator,
resetPropagator
);
// 2. Define internal elements and buses
const inputBus = circuit.boolean.factories.createBusElement(2);
const outputBus = circuit.boolean.factories.createBusElement(1);
const andElement = circuit.boolean.factories.createAndElement(2);
const notElement = circuit.boolean.factories.createNotElement();
// 3. Wire internal components
connectionManager.connect(inputBus.outputs[0], andElement.inputs[0]);
connectionManager.connect(inputBus.outputs[1], andElement.inputs[1]);
connectionManager.connect(andElement.outputs[0], notElement.inputs[0]);
connectionManager.connect(notElement.outputs[0], outputBus.inputs[0]);
// 4. Create the composite element
const customNand = factory.createComposite(inputBus, outputBus);
// 5. Use it
customNand.init();
customNand.inputs[0].value = true;
customNand.inputs[1].value = true;
// Propagate internally (calls SignalPropagator.propagate under the hood)
customNand.propagate();
console.log(customNand.outputs[0].value); // falseComposite elements allow you to use complex components like an RS-Trigger or a half-adder provided by the library.
import * as circuit from 'circuit-ts';
const connectionManager = circuit.boolean.factories.createConnectionManager();
const signalPropagator = circuit.boolean.factories.createSignalPropagator();
const resetPropagator = circuit.boolean.factories.createResetElementPropagator();
const factory = new circuit.boolean.factories.CompositeElementFactory(
connectionManager,
signalPropagator,
resetPropagator
);
// Create a NOR-based RS Trigger
const rsTrigger = factory.createRsTriggerNotOrBased();
rsTrigger.init();
// Inputs: [0] = R (Reset), [1] = S (Set)
// Outputs: [0] = Q, [1] = !Q
rsTrigger.inputs[1].value = true; // Set = 1
// propagate(index) triggers propagation starting from the specific input
rsTrigger.propagate(1);
console.log(rsTrigger.outputs[0].value); // true (Q = 1)The library is fully generic and can work with types other than boolean. Here is an example of a simple Adder element that works with numbers.
import * as circuit from 'circuit-ts';
// 1. Define a custom element for addition
class AdditionElement extends circuit.elements.UniformElement<number> {
constructor() {
super(2, 1, 0); // 2 inputs, 1 output, default value 0
}
public propagate(index?: number): circuit.PortInterface<number>[] {
// Logic: sum all inputs
this.outputs[0].value = this.inputs.reduce((sum, input) => sum + input.value, 0);
return super.propagate(index);
}
}
// 2. Setup
const signalPropagator = new circuit.propagators.SignalPropagator<number>();
const adder = new AdditionElement();
adder.init();
// 3. Use
adder.inputs[0].value = 10;
adder.inputs[1].value = 25;
signalPropagator.propagate(adder.inputs);
console.log(adder.outputs[0].value); // 35connect(output, input): Creates a link between an output and an input.disconnect(output, input): Removes an existing link.
propagate(targets): Propagates signals starting from the given ports (usually inputs that have changed).constructor(visitCounterLimit?: number): Default limit is 100 to prevent infinite loops.
propagate(element): Recursively marks all ports within the element (including internal ones for CompositeElements) as dirty.
- Calculates element logic and returns dirty outputs. For
CompositeElement, it also triggers internal signal propagation.
createConnectionManager(): Returns aConnectionManager<boolean>.createSignalPropagator(): Returns aSignalPropagator<boolean>.createResetElementPropagator(): Returns aResetElementPropagator<boolean>.createAndElement(inputsCount): Returns anAndElement.createOrElement(inputsCount): Returns anOrElement.createNotElement(): Returns aNotElement.createBusElement(channelsCount): Returns aBusElement<boolean>.
createComposite(inputBus, outputBus): Returns a genericCompositeElement.createNotOr(inputsCount): Returns a NOR gate.createNotAnd(inputsCount): Returns a NAND gate.createRsTriggerNotOrBased(): Returns an RS Trigger.
The library provides specific error classes for common issues:
InfiniteLoopError: Thrown when a signal circles back too many times.DuplicateConnectionError: Thrown when trying to connect the same ports twice.InputAlreadyConnectedError: Thrown when an input port already has a source.InvalidPortsPairError: Thrown when trying to connect two inputs or two outputs.
npm i
npm run testCircuit TS is licensed under the MIT License.