
Hello everyone, I’m Harkeerat Singh (Harkeerat24), and this is my final project report for Google Summer of Code 2026 with CircuitVerse for the project Structured Format for Saved Circuit Data.
TL;DR: Over this GSoC, I built and hardened a new Canonical v1 project format for CircuitVerse. It can deterministically export projects, rebuild them through a typed import pipeline, preserve real-world circuit topology and project state, automatically generate layouts with ELK.js when layout data is missing, and validate incoming files using JSON Schema + Ajv. The pipeline was also tested against all 12 Editor’s Picks projects.
In CircuitVerse, a user places components such as inputs, logic gates and outputs on a canvas, connects them with wires, and runs the circuit. Saving a project must capture enough information to open it again.
The project started with a simple problem, circuits with the same components and connections could produce different saved data depending on the order in which they were built or stored.
That makes saved circuit data difficult to compare, hash, validate, migrate, or safely use as a stable format for future tools.
The goal was to introduce a structured canonical representation where the circuit’s logical structure has a deterministic identity, while still preserving the information needed to reconstruct the complete CircuitVerse project.
| Term | Meaning in CircuitVerse |
|---|---|
| Project | The complete saved work, which can contain several circuit tabs. |
| Scope | The simulator’s internal object for one circuit tab, including its components, wires and settings. |
| SubCircuit | A component that uses another circuit in the project as a reusable building block. |
| Port | A connection point on a component, such as a gate’s input or output. |
| Net | A net groups ports that are electrically connected. |
| Netlist | A netlist records the components and those connections. |
| Layout and routing | Where components are placed and how wires travel between them, including bends and branching points. |
| Canonical representation / hash | A representation arranged by defined rules, and a SHA-256 fingerprint calculated from selected structural data for comparison. |
Here, “same structure” means the same components, relevant properties and connections with the same input/output interface order.
Legacy data in this report means the existing JSON save structure used by CircuitVerse’s earlier save/load path. It stores a project’s circuits in a scopes array, with component data and node connections tied to the simulator’s internal objects. The existing generateSaveData() and saveScope() functions write that representation; load() and loadScope() rebuild it.
The frontend repository also has separate simulator implementations. This work is integrated into the V1 simulator, whose code lives under v1/. That simulator version is separate from Canonical v1, which names the first version of the new data format and is identified by formatVersion: "v1" inside the JSON.
A .cv file is a CircuitVerse project file. In this export flow, it contains Canonical JSON as text.
Canonical v1 now forms an end-to-end pipeline around:
The export pipeline turns the simulator’s in-memory objects into Canonical data. Its main entry points are canonicaliseProject() for a whole project and canonicaliseScope() for one circuit.
canonicaliseProject() collects all circuit scopes and builds the SubCircuit dependency graph.khansAlgorithm() orders those scopes child-first. If a dependency cycle exists, export stops.canonicaliseScope(), which runs the scope-level canonical pipeline:indexNodes() maps live simulator nodes to stable indices.discoverNets() uses Union-Find to group electrically connected nodes into nets.buildComponentDrafts() reads component ports, properties and default state.buildStructuralComponentData() prepares the structure used for hashing and sorting.wlFingerprint() performs 1-WL structural refinement: it repeatedly describes each component using its own properties and its connected neighbours.canonicalSort() uses 1-WL structural fingerprints to sort non-interface components while preserving the Input/Output interface order.assignComponentIds() assigns stable component IDs.attachComponentPorts() and buildCanonicalNets() attach component ports and assign stable net IDs.buildLayout() stores routing, intermediate nodes, port positions and annotations, while buildVisual() stores canvas state.buildCanonicalComponents() creates the final component representation.sha256() creates the structural hash for that scope. Child scope hashes are then used when hashing SubCircuit components, so their identity depends on the child circuit itself rather than a temporary scope ID.canonicaliseProject(), the ordered scopes are placed into the final circuits map, the scope hashes form the project hash, buildProjectMetadata() adds the project name, clock, focused circuit and tab order, and the final CanonicalProject is returned.The import side follows the reverse path: validate the Canonical project, rebuild its scopes and wiring, then canonicalise it again to make sure the reconstructed project still has the same identity.
importCanonical() receives the project. In the validation layer, validateCanonicalJson() first checks the JSON Schema and project cross-references before import state is modified.computeImportOrder() uses the same dependency idea to make sure child circuits are reconstructed before parents that use them as SubCircuits.newCircuit() creates the scope and importSingleScope() rebuilds it:buildComponents() creates the component instances and restores saved port positions.generateElkLayout() generates it with ELK.js.applyComponentLayout() places the components.buildAnnotations() restores annotations.wireComponents() reconnects simple nets.restoreDefaultState() restores saved component state.restoreIntermediateNodes() reconstructs routed wires and junctions.restoreScopeMetadata() restores the circuit metadata and canvas state.verifyRoundTrip() canonicalises each reconstructed scope again and compares its hash with the original scope hash.canonicaliseProject() runs once more and compares the complete project hash with the original Canonical v1 project.The pipeline is connected to the V1 simulator UI. Canonical projects can be:
.cv filesIn ExportProject.vue, generateExport() calls canonicaliseProject() and formats the result as JSON. onCopy() attempts to copy that text, and onDownload() saves it with a .cv extension.
In ImportProject.vue, importDataFromFile() starts file selection or reading, and receivedText() parses the JSON and calls importCanonical(). Before importing, it takes a Canonical snapshot of the current project. If the importer returns a failure, the UI attempts to restore that snapshot and reports any restoration failure. This recovery belongs to the UI wrapper; schema and dependency checks inside importCanonical() happen before the existing scopes are reset.

CircuitVerse’s Editor’s Picks is a curated collection of community projects. I used 12 saved projects from that collection, including CPUs, a memory circuit and a tic-tac-toe simulator, to exercise the pipeline with more complex wiring and nested circuits than small hand-built examples.
The project files are checked into the repository as fixtures, meaning fixed test inputs. This keeps the tests repeatable without relying on a live website request or later changes to the featured projects.
For every fixture, the same complete path is tested:
legacy project → canonical export → canonical import → canonical re-export → comparison
The real-project fixtures verify that Canonical v1 preserves:
The saved representation preserves the complete project data while keeping the structural hash focused on canonical identity. Routing is represented through unified connections that can point to either a component port or an intermediate routing node.
Result: All 12 Editor’s Picks projects pass the complete round-trip tests. 🎉

A valid Canonical v1 project does not have to contain layout data. If layout exists, the importer preserves it exactly. If it is missing, the pipeline can generate a readable layout automatically using ELK.js.
The layout rule is:
When layout is missing, the Auto Layout adapter follows a small pipeline of its own:
importSingleScope() first calls buildComponents(), then passes the instances to generateElkLayout(). This gives the adapter actual component dimensions and port positions.buildElkGraph() uses buildElkNodes() and buildElkEdges() to translate components and nets. Inputs are placed toward the first layer and Outputs toward the last, with ports fixed relative to their component.buildElkEdges() represents fan-out with a temporary zero-size junction, giving ELK simple edges to route around one shared branching point.generateElkLayout() calls getElk().layout(graph). The worker runs a layered layout from left to right with orthogonal routing, meaning horizontal and vertical wire segments.buildIntermediateNodes() converts bend and junction points into CircuitVerse routing. getOrAddNode() and addConnection() avoid duplicates, while snap() aligns generated coordinates with the simulator grid.autoLayout.ts builds the graph, runs ELK, and converts the result to a CanonicalLayout. importCanonical.ts constructs components, applies the layout, and restores intermediate nodes through the normal import flow.These layout helpers are implemented in autoLayout.ts.
Fan-out needs an additional adapter because ELK works best with simple source-to-target edges while a CircuitVerse net can contain one source connected to several targets.
The Auto Layout adapter therefore uses a temporary zero-size junction to preserve one shared branching point during layout. After ELK finishes, that temporary representation is converted back into CircuitVerse’s normal intermediate routing nodes and bend points.
Generated coordinates are deduplicated and snapped to the simulator grid, and the ELK-specific junction never becomes part of Canonical v1 itself.
The final Canonical v1 pipeline includes a strict runtime validation layer so imported project data is checked before it can affect the simulator. The JSON Schema defines the structure the format accepts, while Ajv and the cross-reference checks make sure that the project is both well-formed and internally consistent.
Together, these checks turn Canonical v1 into a format that the importer can trust instead of simply assuming that incoming JSON is valid.
The schema follows JSON Schema Draft 2020-12 and uses reusable $ref definitions with constraints for:
validateSchema(), compiled by Ajv, checks the structure. formatSchemaError() turns failures into readable errors with paths to the relevant fields.validateProjectReferences() checks the relationships inside the project: circuits, subcircuits, components, nets, layouts, routing nodes and routing endpoints must all point to valid data.Validation happens before importCanonical() modifies project state, and errors include useful JSON paths so invalid data is easier to locate.
layout remains optional because a schema-valid project without layout can continue into the ELK.js Auto Layout path.
One useful result of Canonical v1 is that a circuit can be described through its components and nets without depending on canvas coordinates. This makes it a useful target for a large language model (LLM) generating circuit descriptions.
The same representation gives LLMs a much cleaner target. Instead of asking a model to generate both circuit logic and exact canvas coordinates, it can generate the Canonical v1 components, nets and SubCircuit relationships and leave layout out. CircuitVerse can validate that JSON, rebuild the logical circuits in dependency order, and let ELK.js place and route them automatically.
For the demo, I used a small hierarchical 2-bit ripple-carry adder with three circuit scopes. The LLM generated a reusable HalfAdder, a FullAdder built from HalfAdder SubCircuit instances, and finally TwoBitAdder from two FullAdder SubCircuit instances.
LLM → hierarchical Canonical v1 .cv without layout → validation → child-first import → ELK.js Auto Layout → working CircuitVerse project
| PR | What it delivered | Status |
|---|---|---|
| #1093 | Initial deterministic canonical export pipeline | Superseded by #1095 |
| #1094 | Multi-circuit canonical project support | Superseded by #1095 |
| #1095 | Deterministic export, subcircuits, Kahn’s Algorithm and cycle detection | Merged |
| #1131 | Canonical import pipeline and round-trip verification | Merged |
| #1132 | Export/import UI integration and rollback flow | Merged |
| #1173 | Strict canonical types and pipeline refactor | Merged |
| #1221 | Complete real-world round trips and 12 Editor’s Picks tests | Merged |
| #1222 | ELK.js Auto Layout for layout-less canonical projects | Merged |
| #1236 | Canonical v1 JSON Schema | Merged |
| #1242 | Ajv + cross-reference runtime validation | Merged |
A few things from this project will definitely stay with me:
Now that Canonical v1 is in place, there are a few areas that can be improved or built on in the future.
I wrote a blog every week, so the complete GSoC journey is documented here:
| Period | Blog |
|---|---|
| Community Bonding | From a Student Who Used CircuitVerse to a GSoC Contributor |
| Week 1 | Canonicalization, Community & a Win |
| Week 2 | Scaling to Multi-Circuit Support |
| Week 3 | Subcircuits, Sorting & Demo Day |
| Week 4 | A Slow Week, Big Progress |
| Week 5 | Back on Track with Import |
| Week 6 | First PR Finally Merged |
| Week 7 | The Full Pipeline Comes Alive |
| Week 8 | Midterms Passed! |
| Week 9 | Rethinking Type Safety |
| Week 10 | Stricter Types, PR Merged |
| Week 11 | Real Circuits Broke My Assumptions |
| Week 12 | T-Shirt, Tests & Auto Layout |
| Week 13 | Auto Layout |
| Week 14 | Schema, AJV & the Final Stretch |
I’m grateful to my mentors Aboobacker MK, Josh Varga and Aryann for their reviews, questions, ideas and support throughout the project. Their feedback constantly pushed me to question assumptions, simplify the design and make the final pipeline stronger.
I’m also very thankful to the CircuitVerse community for the trust, feedback and support throughout the summer, and to Google Summer of Code for giving me the opportunity. I learnt a huge amount from building, testing and refining this project, and I’m genuinely proud of the final result we were able to put together. 🩵