Skip to content

feat(python): load visual circuits directly in Context - #3291

Merged
Mine Starks (minestarks) merged 9 commits into
microsoft:mainfrom
tzh476:feat/load-visual-circuit
Jun 11, 2026
Merged

feat(python): load visual circuits directly in Context#3291
Mine Starks (minestarks) merged 9 commits into
microsoft:mainfrom
tzh476:feat/load-visual-circuit

Conversation

@tzh476

@tzh476Stephen Smith (tzh476) commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Fixes#3232.

Summary

  • add Context.import_circuit(path, *, index=0, name=None, program_type=ProgramType.File) for standalone .qsc visual-circuit files
  • support selecting a circuit from multi-circuit .qsc files with index=0 by default
  • default ProgramType.File imports a single zero-argument Q# operation named from the file stem, or from the optional name= override, suitable for ctx.run(...) and ctx.circuit(...)
  • support ProgramType.Operation for registering the qubit-taking circuit operation directly so it can be composed from Q# entry expressions
  • generate the standalone File-mode Q# operation in the native Rust layer via qsc_circuit, avoiding Python-side helper/wrapper cleanup and avoiding extra public helper callables in qdk.code
  • re-export ProgramType from qdk for the new import mode selection
  • cover direct visual-circuit import, multi-circuit selection, Operation mode, and explicit name override in Python tests; add native qsc_circuit tests for standalone Q# generation

AI Assistance Disclosure

I used Codex to help inspect the existing QDK visual-circuit conversion path, draft the Python/Rust integration changes, respond to review feedback, and prepare tests. I manually reviewed the patch and verified it locally with the checks below.

Testing

Latest post-rebase validation on 66e996fe5:

./build.py --qdk --test --no-check-prereqs --no-check

Result: the QDK Python wheel built successfully, and the Python package test suite completed with 1401 passed, 84 skipped, 1 xfailed.

Additional targeted checks run during the native Rust-layer refactor:

RUSTUP_HOME=/Users/bytedance/Library/Caches/puccinialin/rustup CARGO_HOME=/Users/bytedance/Library/Caches/puccinialin/cargo cargo fmt --check --all
cargo test -p qsc_circuit circuit_to_qsharp
source/qdk_package/.venv/bin/python -m pytest source/qdk_package/tests/test_project.py -k import_circuit -q
python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/qdk/_native.pyi source/qdk_package/tests/test_project.py
git diff --check

@tzh476

Copy link
Copy Markdown
ContributorAuthor

@microsoft-github-policy-service agree

@tzh476

Copy link
Copy Markdown
ContributorAuthor

Hi Bill Ticehurst (@billti)Ian Davis (@idavis)Mine Starks (@minestarks), I have signed the CLA. Could you please approve the pending workflows and review when convenient? Thanks!

Comment threadsource/qdk_package/qdk/_context.py Fixed
@tzh476

Copy link
Copy Markdown
ContributorAuthor

I addressed the GitHub Advanced Security / DevSkim review in c53b195 by documenting the generated-source boundary and adding the targeted DS189424 suppression on the vetted Q# eval line. The review thread is resolved now. Could you please approve the refreshed workflows when convenient? Thanks!

@tzh476

Copy link
Copy Markdown
ContributorAuthor

All refreshed checks are passing now, including Build and test, Rust Unit tests, Integration tests, DevSkim, and the CLA check. The prior GitHub Advanced Security / DevSkim review thread is resolved as well.

Could you please review this when convenient? Thanks!

@minestarks

Copy link
Copy Markdown
Contributor

Thank you Stephen Smith (@tzh476) ! I'm taking a look now - will post comments shortly.

@minestarksMine Starks (minestarks) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice contribution, thank you! Just a couple of requests, please.

Comment threadsource/qdk_package/qdk/_context.py Outdated
Comment threadsource/qdk_package/qdk/_context.py
@tzh476

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review. I pushed cc107d855 to address both requests:

  • renamed the visual circuit API to import_circuit;
  • added program_type with ProgramType.File as the default wrapper behavior and ProgramType.Operation for registering the circuit operation itself for Q# entry-expression composition;
  • updated the existing visual-circuit tests and added an Operation-mode test using a string entry expression.

Local validation:

python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/tests/test_project.py
git diff --check

I attempted the targeted pytest locally, but this checkout cannot resolve the repository-local qsharp==0.0.0 dependency through uv outside the full build environment, so I am relying on CI for the native-backed test run.

Comment threadsource/qdk_package/qdk/_context.py Outdated
Comment threadsource/qdk_package/qdk/_context.py
Comment threadsource/qdk_package/qdk/_context.py Outdated
@minestarks

Copy link
Copy Markdown
Contributor

Stephen Smith (@tzh476) thank you for the edits! It's very close! I've been playing around with it on my machine, and found a couple of minor user-facing API/behavior concerns (see comments). Thank you!

@tzh476

Copy link
Copy Markdown
ContributorAuthor

Thanks for the follow-up. I pushed e030fde0b to address the two actionable items:

  • import_circuit now defaults the generated operation base name to the file stem and accepts an optional name= override.
  • ProgramType is now re-exported from the qdk package root, and the visual-circuit operation test imports it with from qdk import ProgramType.

I also updated the operation-mode expectation from circuit_0 to circuit, and added a small name= override test.

Local validation:

python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/tests/test_project.py
git diff --check

The local checkout here still does not have the built qdk._native extension / pytest environment available, so I am relying on CI for the native-backed test execution.

@tzh476

Stephen Smith (tzh476) commented Jun 9, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks for the clarification from Stefan J. Wernli (@swernli). I pushed 3726daec4 to align the visual circuit import behavior with import_openqasm semantics:

  • ProgramType.File now returns the standalone callable using the requested/default name, without the extra public _Entry callable.
  • The generated qubit-taking circuit operation is marked internal and used only as the File-mode wrapper implementation detail.
  • ProgramType.Operation still returns the qubit-taking operation directly for composition from Q# entry expressions.
  • Added tests covering the public callable names and absence of the old _Entry / helper public callables for File mode.

Local validation passed:

python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/tests/test_project.py
git diff --check

The native-backed targeted pytest needs the repository build/test environment, so I am relying on the refreshed GitHub CI run for that coverage. Could you please approve the refreshed workflows and review when convenient? Thanks!

@minestarks

Copy link
Copy Markdown
Contributor

Stephen Smith (@tzh476) hmm, the CI checks failed. You can run the same CI checks by just running ./build.py locally .

The problem seems to be that marking the operation internal doesn't actually hide it from qdk.code module.

Let's try to address it deeper within the native code / Rust layer. We can refactor things a bit. Can we avoid generating 2 operations altogether, and have just ONE named Foo? (No suffixes, not Foo_Operation etc) .

It seems that the solution would be to refactor build_operation_def in the qsc_circuit crate into two functions: one that generates the body block of the operation, and one that generates the signature + length check. Then, when we want to make a stand-alone operation that doesn't take qubits: it can use a new function that generates the same body block, but uses that as an expression inside a wrapper that allocates the qubits instead of taking them as an argument. That should result in only one Q# operation ever being defined.

See unit tests at source/compiler/qsc_circuit/src/circuit_to_qsharp/tests.rs to understand behavior of build_operation_def

@tzh476

Copy link
Copy Markdown
ContributorAuthor

I saw the refreshed Build and test CI failed because the helper operations were still exposed through ctx.code even after being marked internal. I pushed 7f6f73a2a to keep those helper callables available inside the interpreter for the File-mode wrapper, but remove them from the Python ctx.code namespace after evaluation.

Local validation passed again:

python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/tests/test_project.py
git diff --check

This should address the two failing assertions from the previous CI run while preserving the public File/Operation behavior discussed above.

@swernli

Copy link
Copy Markdown
Contributor

Actually, I realized a simple approach you could use to effectively make the operation internal without having to modify the code module after the fact: define the operation inside of the other operation. This restricts the visibility to within the wrapper. So when the inner operation is defined alongside the wrapper, both are visible:

image

but when the inner operation is defined inside of the wrapper, only the wrapper will be visible:

image

So rather than putting the two operations next to each other, put one inside of the other.

@tzh476

Stephen Smith (tzh476) commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

Superseded by the native Rust-layer follow-up in 87c37b709 below. The previous note about missing local pytest is no longer current; I set up the package venv and ran the repository build/test flow locally.

@tzh476

Copy link
Copy Markdown
ContributorAuthor

Thanks for the detailed guidance. I replaced the Python-side wrapper/helper approach with a native Rust-layer standalone path in 87c37b709.

What changed:

  • qsc_circuit now has circuit_to_standalone_qsharp(file_name, circuits_json, index) for File mode. It builds only the selected circuit as one zero-argument operation named exactly Foo.
  • build_operation_def now shares the circuit body generation with the standalone path, so File mode allocates/resets qubits inside the single generated operation instead of defining helper operations.
  • ProgramType.Operation still uses the existing qubit-array operation generation for Q# composition.
  • Added qsc_circuit tests for standalone visual circuit generation and updated the Python import tests.

Local validation passed:

./build.py --qdk --test --no-check-prereqs --no-check
RUSTUP_HOME=/Users/bytedance/Library/Caches/puccinialin/rustup CARGO_HOME=/Users/bytedance/Library/Caches/puccinialin/cargo cargo fmt --check --all
cargo test -p qsc_circuit circuit_to_qsharp
source/qdk_package/.venv/bin/python -m pytest source/qdk_package/tests/test_project.py -k import_circuit -q
python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/qdk/_native.pyi source/qdk_package/tests/test_project.py
git diff --check

The linked failed Status Check job was the aggregator; the underlying Build and test failure was caused by the exposed helper operations. This update removes that helper-operation surface instead of trying to hide it from qdk.code.

Could you please approve the refreshed workflows and review when convenient? Thanks!

@tzh476

Copy link
Copy Markdown
ContributorAuthor

Quick status update after the native Rust-layer refactor in 87c37b709: the branch now generates a single standalone Q# operation for File mode instead of Python-side helper/wrapper cleanup, matching the direction discussed above. The latest visible check is only the CLA check, so I believe the refreshed workflows still need approval/re-run for the new commit. Could you approve the workflows and re-review when convenient? Thanks!

Change-Id: I3b8a9eadb78dbe1f61b1ebb058a78bc1b0985a29
Change-Id: Id6234644a0f42eab2b6e6472df0464521947c71a
Change-Id: I962310d4ae6aaf624aee2632d0d0f5bb7b589328
Change-Id: I07504f28c2001e92d220f96d3bca5dff492fdbf7
Change-Id: I4c9feeb6d09968b436fdb85ea71c47b8f414310e
Change-Id: I3f2af6e35350c9716b1c8c31dbf018e7d338b104
Change-Id: I5b3e04d51a22a1760a10b95e7b69b2b547a7113e
Change-Id: I976285e0e7a28b0249632f6ba5bafc545ee65670
@tzh476

Stephen Smith (tzh476) commented Jun 11, 2026

Copy link
Copy Markdown
ContributorAuthor

Follow-up: I rebased the branch onto the latest microsoft/qdk:main and force-pushed the updated head 66e996fe5. The PR diff is still limited to the same visual-circuit import files.

Post-rebase local validation now passes with the repository package build/test flow:

./build.py --qdk --test --no-check-prereqs --no-check

Result: the QDK Python wheel built successfully, and the Python package test suite completed with 1401 passed, 84 skipped, 1 xfailed.

The only currently visible check is still the CLA check, so the refreshed workflows appear to need approval/re-run for the rebased head as well.

@minestarksMine Starks (minestarks) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested. One last round of minor cleanup comments - I think the user-facing API and behavior are all there. Thank you!

Comment threadsource/qdk_package/qdk/_context.py Outdated
Comment threadsource/qdk_package/tests/test_project.py Outdated
Comment threadsource/qdk_package/tests/test_project.py Outdated
Change-Id: Ib5a34348e3c49d941dcd457b47117016dfa17fef
@tzh476

Copy link
Copy Markdown
ContributorAuthor

Pushed 35e466d77 to address the final cleanup review comments.

Changes:

  • Reduced the old visual-circuit signature helper to _visual_circuit_count, which only reads the top-level circuit count needed for multi-circuit ProgramType.Operation callable selection.
  • Removed the now-unnecessary not hasattr assertions for old helper names.
  • Added a regression test for importing a selected operation from a multi-circuit visual circuit file.

Local validation passed:

python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/tests/test_project.py
git diff --check
source/qdk_package/.venv/bin/python -m pytest source/qdk_package/tests/test_project.py -k import_circuit -q
./build.py --qdk --test --no-check-prereqs --no-check

Full build/test result: 1402 passed, 84 skipped, 1 xfailed.

I also replied to and resolved the three review threads.

@minestarks
Mine Starks (minestarks) added this pull request to the merge queueJun 11, 2026
Merged via the queue into microsoft:main with commit c7368e2Jun 11, 2026
12 checks passed
João Boechat (joao-boechat) pushed a commit that referenced this pull request Jun 16, 2026
Fixes#3232.
- add `Context.import_circuit(path, *, index=0, name=None,
program_type=ProgramType.File)` for standalone `.qsc` visual-circuit
files
- support selecting a circuit from multi-circuit `.qsc` files with
`index=0` by default
- default `ProgramType.File` imports a single zero-argument Q# operation
named from the file stem, or from the optional `name=` override,
suitable for `ctx.run(...)` and `ctx.circuit(...)`
- support `ProgramType.Operation` for registering the qubit-taking
circuit operation directly so it can be composed from Q# entry
expressions
- generate the standalone File-mode Q# operation in the native Rust
layer via `qsc_circuit`, avoiding Python-side helper/wrapper cleanup and
avoiding extra public helper callables in `qdk.code`
- re-export `ProgramType` from `qdk` for the new import mode selection
- cover direct visual-circuit import, multi-circuit selection, Operation
mode, and explicit name override in Python tests; add native
`qsc_circuit` tests for standalone Q# generation
I used Codex to help inspect the existing QDK visual-circuit conversion
path, draft the Python/Rust integration changes, respond to review
feedback, and prepare tests. I manually reviewed the patch and verified
it locally with the checks below.
Latest post-rebase validation on `66e996fe5`:
```bash
./build.py --qdk --test --no-check-prereqs --no-check
```
Result: the QDK Python wheel built successfully, and the Python package
test suite completed with `1401 passed, 84 skipped, 1 xfailed`.
Additional targeted checks run during the native Rust-layer refactor:
```bash
RUSTUP_HOME=/Users/bytedance/Library/Caches/puccinialin/rustup CARGO_HOME=/Users/bytedance/Library/Caches/puccinialin/cargo cargo fmt --check --all
cargo test -p qsc_circuit circuit_to_qsharp
source/qdk_package/.venv/bin/python -m pytest source/qdk_package/tests/test_project.py -k import_circuit -q
python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/qdk/__init__.py source/qdk_package/qdk/_native.pyi source/qdk_package/tests/test_project.py
git diff --check
```
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow running visual circuit files (.qsc) directly from Python

4 participants

@tzh476@minestarks@swernli@github-advanced-security