Skip to content

Conversation

@swernli
Copy link
Collaborator

@swernli swernli commented Dec 4, 2025

This change adds a parameter to Q# and OpenQASM circuit generation called prune_classical_qubits that will generate a simplified circuit diagram where qubits that are unused or purely classical (never enter superposition) are removed. This can be handy for places where the program comes from generated logic that may use more qubits than strictly needed. This is purely for visualizing the simplified circuit and does not perform any kind of execution optimization.

So a program like this that has nine qubits but doesn't really use all of them:
image

will by default show the full circuit as it does today:
image

but with prune_classical_qubits = True will remove the purely classical q1, q2, q4, q5, and q8:
image

@DmitryVasilevsky
Copy link
Contributor

DmitryVasilevsky commented Dec 4, 2025

I'm a bit uneasy about this feature as it is. The trimmed circuit is not equivalent to the original and it may be confusing. For example, I can create this circuit:

operation Foo() : Result {
    use qs = Qubit[2];
    X(qs[0]);
    CNOT(qs[0], qs[1]);
    H(qs[1]);
    Ry(Std.Math.PI() / 2.0, qs[1]);
    X(qs[0]);
    MResetZ(qs[1])
}

In this circuit the first qubit is trimmable, but the second is not. We can compare original and resulting circuits.
Original:
image
Trimmed:
image
For the original circuit the result is always Zero. But for the trimmed circuit the result is always One.
I would rather see CNOT replaced with X in the resulting diagram, but that would not be easy. I think the feature still has merits as it is, but then I don't like the name of the parameter 'trim'. It feels like the diagrams should be equivalent. I think we need a scarier name for this parameter.
@minestarks already mentioned a different name would be better, but it was only from the 'trimming' point. I think the fact that the circuit becomes not equivalent should also be reflected in the name.

@swernli swernli changed the title Add optional trim setting for circuit generation Add optional prune_classical_qubits setting for circuit generation Dec 6, 2025
@swernli swernli force-pushed the swernli/circuit-trimming branch from 920cd6c to 260bf32 Compare December 6, 2025 03:01
@swernli
Copy link
Collaborator Author

swernli commented Dec 6, 2025

... I would rather see CNOT replaced with X in the resulting diagram, but that would not be easy. I think the feature still has merits as it is, but then I don't like the name of the parameter 'trim'. It feels like the diagrams should be equivalent.

@DmitryVasilevsky, I believe the new logic handles this appopriately. Using your same sample code, the output now looks the way you were hoping, keeping the circuit equivalent:
image

I'll need more tests to ensure this across different inputs though!

This change adds a parameter to Q# and OpenQASM circuit generation called `trim` that will generate a simplified circuit diagram where qubits that are unused or purely classical (never enter superposition) are removed. This can be handy for places where the program comes from generated logic that may use more qubits than strictly needed. This is purely for visualizing the simplified circuit and does not perform any kind of execution optimization.
@swernli swernli force-pushed the swernli/circuit-trimming branch from 260bf32 to 88b2c87 Compare December 8, 2025 21:28
@swernli
Copy link
Collaborator Author

swernli commented Dec 8, 2025

@DmitryVasilevsky @minestarks This is now ready for review with the updated behavior based on your feedback. No rush to get this in soon though.

finish_circuit(qubits, operations, num_qubits, source_lookup)
}

fn should_keep_operation_mut(&self, op: &mut Operation) -> bool {
Copy link
Member

Choose a reason for hiding this comment

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

Can this operate on OperationOrGroup before we transform it into Operation? OperationOrGroup is the "working" data structure which is more ergonomic to manipulate, and already has methods like all_qubits() . Operation is essentially the final "wire format" that we serialize to JSON, so we have to keep it stable but is kind of annoying to manipulate. You shouldn't have to change this function too much.

Comment on lines +217 to +238
#[must_use]
pub fn targets_mut(&mut self) -> &mut Vec<Register> {
match self {
Operation::Measurement(m) => &mut m.qubits,
Operation::Unitary(u) => &mut u.targets,
Operation::Ket(k) => &mut k.targets,
}
}

#[must_use]
pub fn all_qubits(&self) -> Vec<Register> {
match self {
Operation::Measurement(m) => m.qubits.clone(),
Operation::Unitary(u) => {
let mut qubits = u.targets.clone();
qubits.extend_from_slice(&u.controls);
qubits
}
Operation::Ket(k) => k.targets.clone(),
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Please see above comment - if you use OperationOrGroup when manipulating the circuit, you shouldn't have to add these methods

circuit_builder: OperationListBuilder,
next_result_id: usize,
user_package_ids: Vec<PackageId>,
superposition_qubits: FxHashSet<usize>,
Copy link
Member

Choose a reason for hiding this comment

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

These are "wire ID"s right? Can we leave the type as QubitWire for clarity?

}

fn mark_qubit_in_superposition(&mut self, wire: QubitWire) {
self.superposition_qubits.insert(wire.into());
Copy link
Member

@minestarks minestarks Dec 15, 2025

Choose a reason for hiding this comment

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

Perhaps we can have an assert here (and in flip_classical_qubit below) that the prune_classical_qubits config is set to true. Reading this, I get a bit nervous that we might be doing this work regardless of config flag. I can see you obviously took it into consideration this in this PR, but it feels like it could be accidentally regressed with a future change.

circuit_builder: OperationListBuilder,
next_result_id: usize,
user_package_ids: Vec<PackageId>,
superposition_qubits: FxHashSet<usize>,
Copy link
Member

Choose a reason for hiding this comment

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

Since this is expected to be very dense with a range of 0..max_qubits ... is FxHashSet<usize> still the right choice or should we use Vec<bool>?

(I can totally buy "it doesn't matter for circuits this small" though)

generation_method: Option<CircuitGenerationMethod>,
source_locations: Option<bool>,
group_by_scope: Option<bool>,
prune_classical_qubits: bool,
Copy link
Member

Choose a reason for hiding this comment

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

I can't process why this flag looks different than the others. Why not Option<bool>? Should the others have been bool?

source_locations: config.source_locations,
max_operations: config.max_operations,
group_by_scope: config.group_by_scope,
prune_classical_qubits: false,
Copy link
Member

Choose a reason for hiding this comment

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

Don't we want to plumb this up to VS Code?

Comment on lines +245 to +246
// We need to pass the original number of qubits, before any trimming, to finish the circuit below.
let num_qubits = qubits.len();
Copy link
Member

Choose a reason for hiding this comment

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

Just curious, why do we need the original num_qubits? Should we need it? I'm not familiar with the code in operation_list_to_grid so I'm genuinely asking to see if there's something that can be improved there.

Copy link
Member

@minestarks minestarks left a comment

Choose a reason for hiding this comment

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

Looks good, but lots of little suggestions

@msoeken
Copy link
Member

msoeken commented Dec 16, 2025

... I would rather see CNOT replaced with X in the resulting diagram, but that would not be easy. I think the feature still has merits as it is, but then I don't like the name of the parameter 'trim'. It feels like the diagrams should be equivalent.

@DmitryVasilevsky, I believe the new logic handles this appopriately. Using your same sample code, the output now looks the way you were hoping, keeping the circuit equivalent: image

I'll need more tests to ensure this across different inputs though!

This is better than the original approach after including @DmitryVasilevsky's feedback, but I still find this confusing. I do like the underlying motivation for the feature though. Could one alternative highlight qubits that are 100% "classical" (either in |0> or |1> state) with some color. Their value might affect different gates/operations in different ways, e.g., |0> on a CNOT's control removes the gate, |1> on a CNOT's control removes the control (other gates behave differently). Further, not meant to be part of this PR, but if we have logic to determine these values, we could also apply the optimization to the circuit and then visualize it (and have the same benefits for other tasks, to, e.g., speed up simulation, reduce resources, ...)

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.

5 participants