Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cirq-web/cirq_web/circuits/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ def get_client_code(self) -> str:
self.serialized_circuit = self._serialize_circuit()

return f"""
<button id="camera-reset">Reset Camera</button>
<button id="camera-toggle">Toggle Camera Type</button>
<div>
<button id="camera-reset">Reset Camera</button>
<button id="camera-toggle">Toggle Camera Type</button>
</div>
<div id="foreign-symbols-wrapper-{stripped_id}" style="display: none;">
<h4>Foreign symbols:</h4>
<ul id="foreign-symbols-list-{stripped_id}"></ul>
</div>
<script>
let viz_{stripped_id} = createGridCircuit(
{self.serialized_circuit}, {moments}, "{self.id}", {self.padding_factor}
Expand All @@ -72,6 +78,17 @@ def get_client_code(self) -> str:
document.getElementById("camera-toggle").addEventListener('click', () => {{
viz_{stripped_id}.scene.toggleCamera(viz_{stripped_id}.circuit);
}});

if (viz_{stripped_id}.circuit.foreign_symbols.length > 0) {{
const wrapper = document.getElementById("foreign-symbols-wrapper-{stripped_id}");
const list = document.getElementById("foreign-symbols-list-{stripped_id}");
wrapper.style.display = 'block';
viz_{stripped_id}.circuit.foreign_symbols.forEach(op => {{
const listItem = document.createElement('li');
listItem.innerText = `Moment ${{op.moment}}: ${{op.wire_symbols.join(', ')}}`;
list.appendChild(listItem);
}});
}}
</script>
"""

Expand Down
21 changes: 19 additions & 2 deletions cirq-web/cirq_web/circuits/circuit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ def test_circuit_client_code(qubit) -> None:
stripped_id = circuit.id.replace('-', '')

expected_client_code = f"""
<button id="camera-reset">Reset Camera</button>
<button id="camera-toggle">Toggle Camera Type</button>
<div>
<button id="camera-reset">Reset Camera</button>
<button id="camera-toggle">Toggle Camera Type</button>
</div>
<div id="foreign-symbols-wrapper-{stripped_id}" style="display: none;">
<h4>Foreign symbols:</h4>
<ul id="foreign-symbols-list-{stripped_id}"></ul>
</div>
<script>
let viz_{stripped_id} = createGridCircuit(
{str(circuit_obj)}, {str(moments)}, "{circuit.id}", {circuit.padding_factor}
Expand All @@ -65,6 +71,17 @@ def test_circuit_client_code(qubit) -> None:
document.getElementById("camera-toggle").addEventListener('click', () => {{
viz_{stripped_id}.scene.toggleCamera(viz_{stripped_id}.circuit);
}});

if (viz_{stripped_id}.circuit.foreign_symbols.length > 0) {{
const wrapper = document.getElementById("foreign-symbols-wrapper-{stripped_id}");
const list = document.getElementById("foreign-symbols-list-{stripped_id}");
wrapper.style.display = 'block';
viz_{stripped_id}.circuit.foreign_symbols.forEach(op => {{
const listItem = document.createElement('li');
listItem.innerText = `Moment ${{op.moment}}: ${{op.wire_symbols.join(', ')}}`;
list.appendChild(listItem);
}});
}}
</script>
"""

Expand Down
2 changes: 1 addition & 1 deletion cirq-web/cirq_web/dist/circuit.bundle.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions cirq-web/cirq_web/src/circuit/components/general_operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2025 The Cirq Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {Group} from 'three';
import {Symbol3D} from './types';

/**
* Class that represents a GeneralOperation.
* A GeneralOperation consists of a sprite
* with location information, and three.js mesh objects
* representing the gates, if applicable.
*/
export class GeneralOperation extends Group {
readonly row: number;
readonly col: number;

/**
* Class constructor.
* @param row The row of the GridQubit
* @param col The column of the GridQubit
*/
constructor(row: number, col: number) {
super();

this.row = row;
this.col = col;
}

/**
* Adds a designated symbol to the qubit. Location information,
* including the moment at which it occurs, is included in the
* symbol itself.
* @param symbol A Symbol3D object to add to the qubit.
*/
addSymbol(symbol: Symbol3D) {
this.add(symbol);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2025 The Cirq Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {expect} from 'chai';
import {Symbol3D, SymbolInformation} from './types';
import {GeneralOperation} from './general_operation';

describe('GeneralOperation', () => {
it('initializes and adds a symbol correctly', () => {
const row = 1;
const col = 2;
const genOp = new GeneralOperation(row, col);

const symbolInfo: SymbolInformation = {
wire_symbols: ['X'],
color_info: ['black'],
moment: 1,
location_info: [{row, col}],
};
const symbol = new Symbol3D(symbolInfo, 1);
genOp.addSymbol(symbol);

const symbol3D = genOp.children[0] as Symbol3D;
expect(symbol3D.children[0].constructor.name).to.equal('X3DSymbol');
});
});
36 changes: 36 additions & 0 deletions cirq-web/cirq_web/src/circuit/grid_circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.
import {Group} from 'three';
import {GridQubit} from './components/grid_qubit';
import {GeneralOperation} from './components/general_operation';
import {Symbol3D, SymbolInformation} from './components/types';

/**
Expand All @@ -25,6 +26,8 @@ export class GridCircuit extends Group {
// rows to <column, GridQubit> pairs.
private qubit_map: Map<number, Map<number, GridQubit>>;
private padding_factor: number;
public foreign_symbols: SymbolInformation[];

/**
* Class constructor
* @param initial_num_moments The number of moments of the circuit. This
Expand All @@ -37,8 +40,13 @@ export class GridCircuit extends Group {
super();
this.padding_factor = padding_factor;
this.qubit_map = new Map();
this.foreign_symbols = [];

for (const symbol of symbols) {
if (symbol.location_info.length === 0) {
this.foreign_symbols.push(symbol);
continue;
}
// Being accurate is more important than speed here, so
// traversing through each object isn't a big deal.
// However, this logic can be changed if needed to avoid redundancy.
Expand All @@ -51,6 +59,10 @@ export class GridCircuit extends Group {
}
this.addSymbol(symbol, initial_num_moments);
}

if (this.foreign_symbols.length > 0) {
this.resolveForeignSymbols(initial_num_moments, padding_factor);
}
}

private addSymbol(symbolInfo: SymbolInformation, initial_num_moments: number) {
Expand Down Expand Up @@ -98,4 +110,28 @@ export class GridCircuit extends Group {
}
return false;
}

private resolveForeignSymbols(initial_num_moments: number, padding_factor: number) {
// Foreign symbols separated addtl. to look distinct from qubits
const currentRow =
this.qubit_map.size > 0 ? Math.max(...this.qubit_map.keys()) + padding_factor + 1 : 0;

for (let moment = 0; moment < initial_num_moments; moment++) {
const symbolsInMoment = this.foreign_symbols.filter(s => s.moment === moment);

symbolsInMoment.forEach((symbol, col) => {
const newSymbol = new Symbol3D(
{
...symbol,
location_info: [{row: currentRow, col}],
},
this.padding_factor,
);

const operation = new GeneralOperation(currentRow, col);
operation.addSymbol(newSymbol);
this.add(operation);
});
}
}
}
116 changes: 116 additions & 0 deletions cirq-web/cirq_web/src/circuit/grid_circuit_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import {GridCircuit} from './grid_circuit';
import {GeneralOperation} from './components/general_operation';
import {Symbol3D, SymbolInformation} from './components/types';
import {expect} from 'chai';

Expand Down Expand Up @@ -109,4 +110,119 @@ describe('GridCircuit', () => {
expect(qubits.length).to.equal(3);
});
});

describe('when a circuit contains symbols with no location info', () => {
const moments = 2;
const symbols: SymbolInformation[] = [
{
wire_symbols: ['X'],
location_info: [{row: 0, col: 0}],
color_info: ['black'],
moment: 1,
},
{
wire_symbols: ['H'],
location_info: [],
color_info: ['red'],
moment: 0,
},
{
wire_symbols: ['Y'],
location_info: [],
color_info: ['blue'],
moment: 0,
},
{
wire_symbols: ['Z'],
location_info: [],
color_info: ['green'],
moment: 1,
},
];

const circuit = new GridCircuit(moments, symbols);
it('creates the correct number of GridQubit children', () => {
const qubits = circuit.children.filter(child => child.constructor.name === 'GridQubit');
expect(qubits.length).to.equal(1);
});

it('creates GeneralOperation objects for each foreign symbol', () => {
const generalOps = circuit.children.filter(
child => child.constructor.name === 'GeneralOperation',
);
expect(generalOps.length).to.equal(3);
});

it('places foreign symbols on a new row below the qubit', () => {
const generalOps = circuit.children.filter(
child => child.constructor.name === 'GeneralOperation',
) as GeneralOperation[];

// There is 1 qubit at row 0. Padding factor defaults to 1.
// The new row should be max_row + padding + 1 = 0 + 1 + 1 = 2
const expectedRow = 2;
for (const op of generalOps) {
expect(op.row).to.equal(expectedRow);
}
});

it('places symbols from the same moment in adjacent columns', () => {
const generalOps = circuit.children.filter(
child => child.constructor.name === 'GeneralOperation',
) as GeneralOperation[];

const opsInMoment0 = generalOps.filter(op => {
const symbol = op.children[0] as Symbol3D;
return symbol.moment === 0;
});
// Sort by column to have a deterministic test
opsInMoment0.sort((a, b) => a.col - b.col);

expect(opsInMoment0.length).to.equal(2);
expect(opsInMoment0[0].col).to.equal(0);
expect(opsInMoment0[1].col).to.equal(1);
});

it('resets column placement for each new moment', () => {
const generalOps = circuit.children.filter(
child => child.constructor.name === 'GeneralOperation',
) as GeneralOperation[];

const opsInMoment1 = generalOps.filter(op => {
const symbol = op.children[0] as Symbol3D;
return symbol.moment === 1;
});

expect(opsInMoment1.length).to.equal(1);
expect(opsInMoment1[0].col).to.equal(0);
});
});

describe('when a circuit contains only symbols with no location info', () => {
const moments = 1;
const symbols: SymbolInformation[] = [
{
wire_symbols: ['H'],
location_info: [],
color_info: ['red'],
moment: 0,
},
];

const circuit = new GridCircuit(moments, symbols);

it('creates only GeneralOperation children', () => {
const generalOps = circuit.children.filter(
child => child.constructor.name === 'GeneralOperation',
);
const qubits = circuit.children.filter(child => child.constructor.name === 'GridQubit');
expect(generalOps.length).to.equal(1);
expect(qubits.length).to.equal(0);
});

it('places the foreign symbol at row 0', () => {
const generalOp = circuit.children[0] as GeneralOperation;
expect(generalOp.row).to.equal(0);
});
});
});