Structured Format for Saved Circuit Data | GSoC 2026 | Final Report

Structured Format for Saved Circuit Data | GSoC 2026 | Final Report

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.


Project Overview

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.

A Few Terms Used in This Report

TermMeaning in CircuitVerse
ProjectThe complete saved work, which can contain several circuit tabs.
ScopeThe simulator’s internal object for one circuit tab, including its components, wires and settings.
SubCircuitA component that uses another circuit in the project as a reusable building block.
PortA connection point on a component, such as a gate’s input or output.
NetA net groups ports that are electrically connected.
NetlistA netlist records the components and those connections.
Layout and routingWhere components are placed and how wires travel between them, including bends and branching points.
Canonical representation / hashA 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, Simulator Versions and Canonical v1

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.

Final Canonical v1 Scope

Canonical v1 now forms an end-to-end pipeline around:

  • deterministic project export and hashing
  • complete multi-circuit and nested subcircuit support
  • canonical import and round-trip verification
  • preservation of routing, project state, annotations and visual metadata
  • strict TypeScript data contracts
  • real-world Editor’s Picks integration tests
  • automatic layout generation using ELK.js
  • JSON Schema and runtime validation using Ajv

Deterministic Canonical Export

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.

Export Flow

  1. canonicaliseProject() collects all circuit scopes and builds the SubCircuit dependency graph.
  2. khansAlgorithm() orders those scopes child-first. If a dependency cycle exists, export stops.
  3. Each scope is passed to 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.
  4. 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.
  5. Back in 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.

Deterministic canonical export architecture


Canonical Import and Round-Trip Verification

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.

Import Flow

  1. importCanonical() receives the project. In the validation layer, validateCanonicalJson() first checks the JSON Schema and project cross-references before import state is modified.
  2. computeImportOrder() uses the same dependency idea to make sure child circuits are reconstructed before parents that use them as SubCircuits.
  3. For every circuit, newCircuit() creates the scope and importSingleScope() rebuilds it:
    • buildComponents() creates the component instances and restores saved port positions.
    • if layout is missing, 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.
  4. verifyRoundTrip() canonicalises each reconstructed scope again and compares its hash with the original scope hash.
  5. After all scopes are imported, canonicaliseProject() runs once more and compares the complete project hash with the original Canonical v1 project.
  6. Only after a successful import does the pipeline restore the tab order, project name, clock settings and focused circuit, then refresh the simulator UI.

Canonical import and round-trip verification architecture

Simulator Integration

The pipeline is connected to the V1 simulator UI. Canonical projects can be:

  • previewed as JSON
  • copied
  • downloaded as .cv files
  • imported back into CircuitVerse

In 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.

Canonical export/import UI

Canonical Pipeline Demo


Real-World Round-Trip Verification

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.

Verification Flow

For every fixture, the same complete path is tested:

legacy project → canonical export → canonical import → canonical re-export → comparison

Data Covered by the Round Trip

The real-project fixtures verify that Canonical v1 preserves:

  • Dangling and standalone wires: wires with an unconnected end, including wires not attached to any component.
  • Fan-out: a single signal branching to several component ports.
  • Routing details: intermediate nodes that record wire bends, junctions and component port positions.
  • Project state: the project name, clock settings, focused circuit and tab order.
  • Editor metadata: annotations and canvas state.
  • Subcircuit metadata: display-related information for subcircuit components.

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. 🎉

All 12 Editor’s Picks round-trip tests passing


Auto Layout with ELK.js

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.

Layout Resolution

The layout rule is:

  • layout present → preserve the saved layout
  • layout missing → run the ELK.js Auto Layout adapter

How I Implemented Auto Layout

When layout is missing, the Auto Layout adapter follows a small pipeline of its own:

  1. Read real component geometry. importSingleScope() first calls buildComponents(), then passes the instances to generateElkLayout(). This gives the adapter actual component dimensions and port positions.
  2. Build the graph. 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.
  3. Handle branching connections. buildElkEdges() represents fan-out with a temporary zero-size junction, giving ELK simple edges to route around one shared branching point.
  4. Calculate placement and routes. generateElkLayout() calls getElk().layout(graph). The worker runs a layered layout from left to right with orthogonal routing, meaning horizontal and vertical wire segments.
  5. Translate the result back. buildIntermediateNodes() converts bend and junction points into CircuitVerse routing. getOrAddNode() and addConnection() avoid duplicates, while snap() aligns generated coordinates with the simulator grid.
  6. The ELK adapter in 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.

ELK.js Auto Layout layout-resolution flow

Fan-Out Routing

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.

Temporary ELK fan-out junction


JSON Schema and Runtime Validation

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.

Structural Validation

The schema follows JSON Schema Draft 2020-12 and uses reusable $ref definitions with constraints for:

  • scope, component and net identifiers
  • port names and port references
  • component and annotation types
  • directions and bit widths
  • routing connections and intermediate nodes
  • project/circuit metadata
  • required fields, numeric limits, enums and extra properties

Cross-Reference Validation

  1. validateSchema(), compiled by Ajv, checks the structure. formatSchemaError() turns failures into readable errors with paths to the relevant fields.
  2. 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.


LLM-Generated Circuit Demo

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


Pull Request Summary

PRWhat it deliveredStatus
#1093Initial deterministic canonical export pipelineSuperseded by #1095
#1094Multi-circuit canonical project supportSuperseded by #1095
#1095Deterministic export, subcircuits, Kahn’s Algorithm and cycle detectionMerged
#1131Canonical import pipeline and round-trip verificationMerged
#1132Export/import UI integration and rollback flowMerged
#1173Strict canonical types and pipeline refactorMerged
#1221Complete real-world round trips and 12 Editor’s Picks testsMerged
#1222ELK.js Auto Layout for layout-less canonical projectsMerged
#1236Canonical v1 JSON SchemaMerged
#1242Ajv + cross-reference runtime validationMerged

What I Learnt

A few things from this project will definitely stay with me:

  • Determinism is much stricter than just sorting things. Comparator rules, graph identity, subcircuit dependencies and tie-breaks all matter when one unstable ordering can change a hash.
  • Real projects are better teachers than happy-path fixtures. The Editor’s Picks tests broke assumptions that looked completely reasonable on smaller circuits.
  • Type safety forces design decisions into the open. Moving away from loose structures made me define what Canonical v1 actually promises instead of letting implementation details stay vague.
  • A schema validates structure; software still has to validate meaning. Ajv can prove that an ID has the right format, but cross-reference validation is what proves that the ID points to something real.
  • Mentor reviews changed the architecture, not just the code. Many of the strongest parts of the final pipeline came from questions raised during review rather than from my first implementation.
  • Stacked PRs are genuinely useful. They let me keep working on dependent features without turning each review into one giant mixed diff.

Potential Future Enhancements

Now that Canonical v1 is in place, there are a few areas that can be improved or built on in the future.

  • Public, versioned Canonical schema: Host the Canonical v1 JSON Schema at a stable URL so external tools can validate CircuitVerse projects without depending on the source repository.
  • RFC 8785 canonicalisation: Enforce the RFC 8785 JSON Canonicalization Scheme so the same circuit always produces byte-for-byte identical Canonical JSON.
  • Legacy migration tool: Build a migration tool that converts existing legacy JSON projects into the new Canonical JSON format.
  • Canonical to Verilog Converter: A tool to convert Canonical JSON to Verilog Code.
  • Circuit diff tooling: Use deterministic component or net IDs and structural hashes to show meaningful logical differences between two versions of a circuit.
  • Incremental canonicalisation and hash caching: Reuse hashes for unchanged circuits to avoid unnecessary recomputation and improve performance.

Weekly Blogs

I wrote a blog every week, so the complete GSoC journey is documented here:

PeriodBlog
Community BondingFrom a Student Who Used CircuitVerse to a GSoC Contributor
Week 1Canonicalization, Community & a Win
Week 2Scaling to Multi-Circuit Support
Week 3Subcircuits, Sorting & Demo Day
Week 4A Slow Week, Big Progress
Week 5Back on Track with Import
Week 6First PR Finally Merged
Week 7The Full Pipeline Comes Alive
Week 8Midterms Passed!
Week 9Rethinking Type Safety
Week 10Stricter Types, PR Merged
Week 11Real Circuits Broke My Assumptions
Week 12T-Shirt, Tests & Auto Layout
Week 13Auto Layout
Week 14Schema, AJV & the Final Stretch

Acknowledgements

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. 🩵

comments powered by Disqus