Skip to content

Commit c1ca1a2

Browse files
minestarksswernli
andauthored
Show source code locations on circuit diagrams (#2761)
This PR adds a feature to circuit diagrams to display Q#/QASM source code locations for each qubit and gate as clickable links in VS Code. They can also be displayed as hover text in the Python circuit widget. https://github.com/user-attachments/assets/fd8203a6-e488-439a-ab65-7e5e18c0928d ## Notes ### Operation call sites Operation boxes are clickable and show hover text with the location of the call site. <img width="240" height="170" alt="image" src="https://github.com/user-attachments/assets/6272cd37-b03c-49be-a693-313b5ff65963" /> We always show the nearest user code location to the actual call site. For example, if the user code shows `ForEach(MResetZ, qs)`, even though the actual `MResetZ` gate is applied in the Std library, the location in the circuit diagram will point to the `ForEach` expression. In Python, "user code" includes both cell contents and any real files pulled via `qsharp.init(project_root=...)` ### Qubit declaration sites Qubit labels are clickable and show hover text with a location. <img width="240" height="170" alt="image" src="https://github.com/user-attachments/assets/6eb69116-e3cf-4842-8639-1021d7cd76fe" /> This location corresponds to the `use` statement that allocated the qubit. Example: use q = Qubit(); There is an exception to this when the circuit diagram is generated for an operation which takes qubit input arguments. In this case, the declaration locations are the input arguments to the operation. Example: operation Test(q1: Qubit, q2: Qubit) : Result[] {} Here, `q0` is declared at 1:16 and `q1` is declared at 1:27. Qubits can have multiple declaration sites. Example: operation Main() : Unit { { use q1 = Qubit(); X(q1); Reset(q1); } { use q2 = Qubit(); Y(q2); Reset(q2); } } The circuit diagram for this code only shows one qubit wire, but it maps to both `q1` and `q2`. So clicking on that qubit label will show both locations: <img width="659" height="350" alt="image" src="https://github.com/user-attachments/assets/5d5e7d9e-cbad-4748-b620-a4e6d10e7f29" /> ### Circuit JSON schema Components now have a `source` field. { "kind": "unitary", "gate": "H", "targets": [{ "qubit": 0 }], "source": { "file": "line_0", "line": 0, "column": 43 } } Qubits have a `declarations` field. { "id": 0, "numResults": 0, "declarations": [{ "file": "foo.ts", "line": 0, "column": 26 }] } These fields don't exist in the .qsc file schema. Even though we don't have a user scenario that writes circuits out to editable .qsc files yet, if we ever do, these fields should be excluded as they lose their meaning when the generated circuit is divorced from its originating source code. ### VS Code In VS code, clicking on an element with a source code location will navigate to the source code. <img width="1090" height="320" alt="image" src="https://github.com/user-attachments/assets/eacae368-7909-45ee-97ad-cd7a7da31d7e" /> ### Python Source code locations can be enabled in Python by passing in `source_locations=True` into `qsharp.circuit()`. <img width="646" height="351" alt="image" src="https://github.com/user-attachments/assets/03bbea91-4636-49ee-b99f-b187f899cc03" /> While the hover text shows the location, clicking on the gates does nothing. Source code locations are not enabled by default in Python. This is because today, the locations for Q#/QASM code evaluated via `qsharp.eval` don't look very meaningful (`line_1:4:2` etc). If a `qsharp.json` is used, then the files will have proper disk locations, but this is a relatively uncommon scenario. ### ASCII art update to show source code location labels, e.g. ASCII art update to show source code location labels, e.g. [email protected]:4:20 ──────── ● ──────── [email protected]:4:20 ──────── ● ──────── [email protected]:4:20 ─ [email protected]:5:20 ── These labels are disabled in the Python string rendering (the only place this ASCII art would be shown to the user) . It's useful in test snapshots (see `circuit_tests.rs` ), but at the moment they look too ugly for public consumption. ### Configuration and enablement Since there is a perf cost to capturing call stacks during circuit tracing, we have to be conservative about when to enable circuit tracing. At this point circuit tracing is only enabled when: - Explicitly generating a circuit, via the "show circuit" command in VS code, or the `circuit()` API in Python - Debugging in VS Code PR forthcoming to decouple the "Run" command from the debugger to avoid the perf hit) - When the Python library is initialized with `trace_circuit=True` Additionally source location capturing is configurable via a user setting. When it's disabled, the Q#/QASM evaluator skips call stack captures. In VS Code, new `settings.json` configurations have been added to tweak circuit generation options. "Q#.circuits.config": { "maxOperations": 10001, "generationMethod": "classicalEval", "sourceLocations": true } While `maxOperations` and `generationMethod` are not new features, they weren't configurable by the user. In Python, these configuration options are exposed in the `circuit()` function: generation_method: Optional[CircuitGenerationMethod] = None, max_operations: Optional[int] = None, source_locations: bool = False, ### Internals #### Evaluator and circuit tracer changes Introduced a `Tracer` trait, now implemented by the circuit tracer (a.k.a. the circuit builder). This trait diverges from `Backend` mainly in that it takes a call stack parameter (`stack: &[Frame]`), which allows the circuit tracer implementation to capture source code metadata in the circuit. The `TracingBackend` composes a regular `Backend`, a `Tracer`, or both, so backends that do not care about tracing remain unchanged. Every time the evaluator makes an intrinsic call, it captures the call stack and passes it to tracer (unless source location capturing is disabled). Also removed `Result` associated type from `Backend` as it made passing this type around needlessly complicated. Implementors can simply return `val::Result`, as the `impl Into<Result>` was enforced everywhere anyway. The mechanics of the circuit tracer is largely unchanged, but it is refactored to make way for future changes. `WireMapBuilder` and `OperationBuilder` are separated out for manageability; these will continue evolving as we implement further circuit diagram features. #### Entry expressions for operation circuits Note that when we generate an circuit for an operation that takes qubits (as opposed to an Entrypoint, which has no input parameters), we have to do some additional processing to figure out qubit declaration locations. Since in this scenario we use a generated entry expression, the real qubit declarations would live there. Example: { use qs = Qubit[2]; (Test.Test)(qs[0], qs[1]); let r: Result[] = []; r } We can't use these as the qubit declaration locations since they don't map to user code, and don't provide any meaningful insight to the user. #### VS Code command A new generic `qsharp-vscode.gotoLocations` command was added to jump to a specific location in source. The built-in VS Code commands didn't play nicely with our UX. #### Circuit rendering If operations or gate labels contain source code locations, they are rendered as clickable SVG elements. The hover text should show the source code location as well. Minor fixes in rendering and CSS (dashed lines were invisible, SWAP gates had an error in the bounding box calculation.) Changed the signature of `draw` to better organize the increasing number of options. --------- Co-authored-by: Mine Starks <> Co-authored-by: Stefan J. Wernli <[email protected]>
1 parent e7bd0d9 commit c1ca1a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+4735
-1591
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

library/src/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod table_lookup;
1717
use indoc::indoc;
1818
use qsc::{
1919
Backend, LanguageFeatures, PackageType, SourceMap, SparseSim,
20-
interpret::{self, GenericReceiver, Interpreter, Result, Value},
20+
interpret::{self, GenericReceiver, Interpreter, Value},
2121
target::Profile,
2222
};
2323

@@ -57,7 +57,7 @@ pub fn test_expression_with_lib_and_profile_and_sim(
5757
expr: &str,
5858
lib: &str,
5959
profile: Profile,
60-
sim: &mut impl Backend<ResultType = impl Into<Result>>,
60+
sim: &mut impl Backend,
6161
expected: &Value,
6262
) -> String {
6363
let mut stdout = vec![];
@@ -106,7 +106,7 @@ pub fn test_expression_fails_with_lib_and_profile_and_sim(
106106
expr: &str,
107107
lib: &str,
108108
profile: Profile,
109-
sim: &mut impl Backend<ResultType = impl Into<Result>>,
109+
sim: &mut impl Backend,
110110
) -> String {
111111
let mut stdout = vec![];
112112
let mut out = GenericReceiver::new(&mut stdout);

samples/notebooks/circuits.ipynb

Lines changed: 19 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"cell_type": "markdown",
2121
"metadata": {},
2222
"source": [
23-
"The `dump_circuit()` function displays a circuit that contains the gates that have been applied in the simulator up to this point."
23+
"You can synthesize a circuit diagram for any program by calling `qsharp.circuit()` with an entry expression."
2424
]
2525
},
2626
{
@@ -35,10 +35,16 @@
3535
"source": [
3636
"%%qsharp\n",
3737
"\n",
38-
"// Prepare a Bell State.\n",
39-
"use register = Qubit[2];\n",
40-
"H(register[0]);\n",
41-
"CNOT(register[0], register[1]);"
38+
"operation GHZSample(n: Int) : Result[] {\n",
39+
" use qs = Qubit[n];\n",
40+
"\n",
41+
" H(qs[0]);\n",
42+
" ApplyToEach(CNOT(qs[0], _), qs[1...]);\n",
43+
"\n",
44+
" let results = MeasureEachZ(qs);\n",
45+
" ResetAll(qs);\n",
46+
" return results;\n",
47+
"}"
4248
]
4349
},
4450
{
@@ -47,7 +53,7 @@
4753
"metadata": {},
4854
"outputs": [],
4955
"source": [
50-
"qsharp.dump_circuit()"
56+
"qsharp.circuit(\"GHZSample(3)\")"
5157
]
5258
},
5359
{
@@ -67,46 +73,6 @@
6773
"source": [
6874
"from qsharp_widgets import Circuit\n",
6975
"\n",
70-
"Circuit(qsharp.dump_circuit())"
71-
]
72-
},
73-
{
74-
"cell_type": "markdown",
75-
"metadata": {},
76-
"source": [
77-
"You can synthesize a circuit diagram for any program by calling `qsharp.circuit()` with an entry expression."
78-
]
79-
},
80-
{
81-
"cell_type": "code",
82-
"execution_count": null,
83-
"metadata": {
84-
"vscode": {
85-
"languageId": "qsharp"
86-
}
87-
},
88-
"outputs": [],
89-
"source": [
90-
"%%qsharp\n",
91-
"\n",
92-
"operation GHZSample(n: Int) : Result[] {\n",
93-
" use qs = Qubit[n];\n",
94-
"\n",
95-
" H(qs[0]);\n",
96-
" ApplyToEach(CNOT(qs[0], _), qs[1...]);\n",
97-
"\n",
98-
" let results = MeasureEachZ(qs);\n",
99-
" ResetAll(qs);\n",
100-
" return results;\n",
101-
"}"
102-
]
103-
},
104-
{
105-
"cell_type": "code",
106-
"execution_count": null,
107-
"metadata": {},
108-
"outputs": [],
109-
"source": [
11076
"Circuit(qsharp.circuit(\"GHZSample(3)\"))"
11177
]
11278
},
@@ -251,22 +217,9 @@
251217
"source": [
252218
"Even though we can't synthesize the above program into a circuit, we still have the option of running it in the simulator, and displaying the resulting circuit.\n",
253219
"\n",
254-
"Note that the resulting circuit diagram shows only one of the two branches that could have been taken."
255-
]
256-
},
257-
{
258-
"cell_type": "code",
259-
"execution_count": null,
260-
"metadata": {
261-
"vscode": {
262-
"languageId": "qsharp"
263-
}
264-
},
265-
"outputs": [],
266-
"source": [
267-
"%%qsharp\n",
220+
"This can be done by passing in `generation_method=GenerationMethod.Simulate` to the `circuit()` function.\n",
268221
"\n",
269-
"ResetIfOne()"
222+
"Note that the resulting circuit diagram shows only one of the two branches that could have been taken."
270223
]
271224
},
272225
{
@@ -275,13 +228,15 @@
275228
"metadata": {},
276229
"outputs": [],
277230
"source": [
278-
"Circuit(qsharp.dump_circuit())"
231+
"from qsharp import CircuitGenerationMethod\n",
232+
"\n",
233+
"Circuit(qsharp.circuit(\"ResetIfOne()\", generation_method=CircuitGenerationMethod.Simulate))"
279234
]
280235
}
281236
],
282237
"metadata": {
283238
"kernelspec": {
284-
"display_name": "Python 3",
239+
"display_name": ".venv",
285240
"language": "python",
286241
"name": "python3"
287242
},
@@ -295,7 +250,7 @@
295250
"name": "python",
296251
"nbconvert_exporter": "python",
297252
"pygments_lexer": "ipython3",
298-
"version": "3.11.11"
253+
"version": "3.14.0"
299254
}
300255
},
301256
"nbformat": 4,

0 commit comments

Comments
 (0)