Skip to content

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

Open
tzh476 wants to merge 5 commits into
microsoft:mainfrom
tzh476:feat/load-visual-circuit
Open

feat(python): load visual circuits directly in Context#3291
tzh476 wants to merge 5 commits into
microsoft:mainfrom
tzh476:feat/load-visual-circuit

Conversation

@tzh476

@tzh476 tzh476 commented Jun 4, 2026

Copy link
Copy Markdown

Fixes #3232.

Summary

  • add Context.load_circuit(path, *, index=0) for standalone .qsc visual circuit files
  • support selecting a circuit from multi-circuit .qsc files with index=0 by default
  • reuse the existing visual-circuit-to-Q# converter through the native Python module
  • register the generated Q# in the same Context and return a zero-argument callable suitable for ctx.run(...) and ctx.circuit(...)
  • cover the direct-load path with a Python test using the existing in-memory filesystem fixture

AI Assistance Disclosure

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

Testing

cargo fmt --all -- --check
cargo check -p qdk
cargo clippy -p qdk --all-targets --all-features -- -D warnings
maturin develop --skip-install
git diff --check
python3 -m py_compile source/qdk_package/qdk/_context.py source/qdk_package/tests/test_project.py
python -m pytest source/qdk_package/tests/test_project.py::test_load_circuit -q
python -m pytest source/qdk_package/tests/test_project.py -q

test_load_circuit passed, and all 12 tests in test_project.py passed.

@tzh476

tzh476 commented Jun 4, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@tzh476

tzh476 commented Jun 4, 2026

Copy link
Copy Markdown
Author

Hi @billti @idavis @minestarks, I have signed the CLA. Could you please approve the pending workflows and review when convenient? Thanks!

Change-Id: I3b8a9eadb78dbe1f61b1ebb058a78bc1b0985a29
@tzh476 tzh476 force-pushed the feat/load-visual-circuit branch from f084b04 to e70656b Compare June 4, 2026 14:39
Comment thread source/qdk_package/qdk/_context.py Fixed
Change-Id: Id6234644a0f42eab2b6e6472df0464521947c71a
@tzh476

tzh476 commented Jun 4, 2026

Copy link
Copy Markdown
Author

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

tzh476 commented Jun 4, 2026

Copy link
Copy Markdown
Author

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
Member

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

@minestarks minestarks left a comment

Copy link
Copy Markdown
Member

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 thread source/qdk_package/qdk/_context.py Outdated
Comment thread source/qdk_package/qdk/_context.py
Change-Id: I962310d4ae6aaf624aee2632d0d0f5bb7b589328
@tzh476

tzh476 commented Jun 5, 2026

Copy link
Copy Markdown
Author

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 thread source/qdk_package/qdk/_context.py Outdated
Comment thread source/qdk_package/qdk/_context.py
Comment thread source/qdk_package/qdk/_context.py Outdated
Comment on lines +610 to +613
wrapper_name = f"{circuit_operation_name}_Entry"
wrapper_source = _visual_circuit_wrapper_source(
circuit_operation_name, wrapper_name, qubit_count, return_type
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question to @swernli - so this results in two callables - Foo and Foo_Entry . When we discussed in person, I hadn't considered that we'd end up with two callables. I actually like it this way - having the inner operation that takes Qubits continue to be called Foo. Thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm... I could see how this would be convenient for advanced users but worry that creating two similarly named callables might make this more confusing for average users. I think matching the behavior of how import_openqasm behaves with regard to ProgramType is easier to explain, where File creates one standalone callable that doesn't take qubit params and Operation creates the callable that takes qubits and can be used along with other code.

@minestarks

Copy link
Copy Markdown
Member

@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!

Change-Id: I07504f28c2001e92d220f96d3bca5dff492fdbf7
@tzh476

tzh476 commented Jun 9, 2026

Copy link
Copy Markdown
Author

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.

Change-Id: I4c9feeb6d09968b436fdb85ea71c47b8f414310e
@tzh476

tzh476 commented Jun 9, 2026

Copy link
Copy Markdown
Author

Thanks for the clarification from @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
Member

@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

Sign up for free to 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