-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add random circuit with graph #12474
base: main
Are you sure you want to change the base?
Conversation
Thank you for opening a new pull request. Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient. While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone. One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 13156564577Details
💛 - Coveralls |
Dear @MozammilQ thanks for showing interest in this issue. I will be on vacation and look into this PR next week! In the meantime make yourself familiar with https://github.com/Qiskit/qiskit/blob/main/CONTRIBUTING.md and the unitaryhack rules (if you have not already). :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added the suggestions :)
@sbrandhsn , please see if this is good enough :) |
@MozammilQ thanks for your interest in looking at issue #12364 ! I agree this issue is a bit tricky but I think your contribution is going into the right direction. :-) Also, thanks for exploring different options for that issue! I did not have time yet to look into your code but wanted to spend the time to clarify some of your questions. To give a little bit of context, the output of this procedure will likely not be executed directly on a quantum computer but will prove valuable for investigating compiler capabilities and efficiencies. The problem I wanted to fix with the You rightfully noted that the generated circuits may become quite large but I think this is fine. I suspect that if you have I think it would be a good idea to raise an error with a reasonable error message if some of the edge weights are none while others are not none. If all edge weights are none, you can assume the weight of each edge to be 1/N. With respects to resets and conditional operations, you can simply follow the approach given in the original Some of your 'reviewer markers' should actually be comments in your code, it would be great if you add those as you see fit. :-) I think the implementation should eventually consider the edge weight as well as consider the I like the idea to fill unused qubits in a layer with a single-qubit gate if Let me know if things became clearer and whether you require any other information. Also, if you make modifications to your PR, let me know for which implementation you went for eventually. :-) |
I have already worked a lot on this PR, so I will surely do some modifications to the code, but, actually right now bit busy in "IBM Quantum Challenge 2024" @sbrandhsn , you explained it very very well, thanks a lot for the time you gave to write and explain these. |
Absolutely! :-) |
This is python so, focus is on code readability. @sbrandhsn , please have a look, and let me know what to change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @MozammilQ, this already looks like it is in good shape! :-) I left a couple of comments. I also think there should be a couple of more test cases with different types of interaction graphs such as sparse graphs, (almost) fully connected and a couple more random ones. Also, I think it would be best if you reused the code in other test cases instead of copying them. See https://networkx.org/documentation/stable/reference/generators.html for how to generate these graphs.
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
for wire in dag.wires: | ||
for dag_op_node in dag.nodes_on_wire(wire, only_ops=True): | ||
if dag_op_node.op.num_qubits == 2: | ||
control, target = dag_op_node.qargs | ||
control_idx = control._index | ||
target_idx = target._index | ||
cp_mp.update({(control_idx, target_idx)}) | ||
|
||
# make sure every qubit-pair from the circuit actually present in the edge_list | ||
for cp in cp_mp: | ||
self.assertTrue(cp in edge_list) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can replace this chunk of code by build_interaction_graph
in https://github.com/Qiskit/qiskit/blob/main/qiskit/transpiler/passes/layout/vf2_utils.py and a subgraph isomorphism check. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have kept the logic untouched, because for some reasons build_interaction_graph
is giving out a coupling map, which is not correct for the given circuit.
On visual inspection, it was confirmed that the coupling map given out by this logic is indeed correct and serve the purpose.
@sbrandhsn , I believe the PR is already inline with your interests. I have just removed Please, have a look and tell me what to change now :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @MozammilQ ! I left a couple of comments - it would be great if you can address them. I like that you improved the code with a lot of comments compared to my last review!
qiskit/circuit/random/utils.py
Outdated
attached to the qubit-pair, the 2Q gates are applied on the qubit-paris on which are no gates | ||
already present for that particular iteration, this is to make sure that for a particular | ||
iteration only one circuit layer exists, now for those qubits for this particular iteration on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this section. If you add gates to qubit-pairs that are not present in the particular iteration, you would end up with a completely dense layer where some qubit-pairs might not be part of the input interaction graph, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1>> extract qubit-pairs from input interaction graph
2>> Iterate over these qubit-pairs, apply 2Q gates but make sure those 2Q gates are
only applied to those qubit-pairs which are idle(both control and target), in case
any control, target found not to be idle, skip to another qubit-pair.
3>> Now, if any qubit still remains idle, apply 1Q if insert_1Q_oper
is enabled.
A qubit which has only reset applied to it is considered idle.
Anyways, provided a better explanation in the docstring.
qiskit/circuit/random/utils.py
Outdated
else: | ||
for current_gate, num_gate_params, edge in zip( | ||
gate_choices["class"], | ||
gate_choices["num_params"], | ||
edge_choices, | ||
): | ||
params = parameters[:num_gate_params] | ||
parameters = parameters[num_gate_params:] | ||
operation = current_gate(*params) | ||
control_qubit, target_qubit = tuple(edge) | ||
|
||
if control_qubit in qubit_idx_not_used and target_qubit in qubit_idx_not_used: | ||
qc._append( | ||
CircuitInstruction( | ||
operation=operation, | ||
qubits=[qubits[control_qubit], qubits[target_qubit]], | ||
) | ||
) | ||
qubit_idx_used.update(set(edge)) | ||
qubit_idx_not_used = qubit_idx_not_used - qubit_idx_used | ||
edges_used[(control_qubit, target_qubit)] += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this branch is not necessary, you can use the logic for conditionals
and layer_idx>0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would led to the calculation of the boolean list of conditionals, even if conditionals is not required :)
This would add a tiny bit of overhead which could have been avoided if this block doesn't run at all for conditional set to False.
That is why I had kept the two separate blocks :)
Anyways, change the code to your liking :)
qiskit/circuit/random/utils.py
Outdated
layer_idx += 1 | ||
|
||
# check if every edge has been used at-least `min_2q_gate_per_edge` number of times. | ||
reached_depth = np.array(list(edges_used.values())) >= min_2q_gate_per_edge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this line be the while
condition in line 285? If yes, you can remove variable stop
and make the condition clearer with a comment in line 284.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to achieve a do-while loop behavior,
There is no need to check the condition for the first time, because its certain that all the qubits would be idle.
That's why I had checked the condition at the very last, and to make the code look pretty used a variable stop
.
Anyways, changed the code to your liking :)
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
for cp in cp_mp: | ||
self.assertTrue(cp in edge_list) | ||
|
||
def test_2q_gates_excluded_edges_with_zero_weight(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am doing code duplication because I want to take a test cp_map
with zero weight for this test only.
|
||
# If any edge weight is zero, just remove that edge from the edge_list | ||
if 0 in edges_probs: | ||
edge_list = [edge for edge, edge_prob in zip(edge_list, edges_probs) if not edge_prob == 0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choosing this logic over "pydi_graph.remove_edge"
@sbrandhsn , please see if its good enough :) |
qiskit/circuit/random/utils.py
Outdated
from qiskit.circuit.exceptions import CircuitError | ||
|
||
|
||
def _get_gates(n_qubits): | ||
return [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have chosen not to return gates as np.ndarray, because it could happen that _get_gates()
is used in some future functions where ndarray is not required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not efficient, it calls the same function as many times as the number of qubits in the gates are allowed.
I still have kept the changes because my opinion is that all the standard gates and closely related stuffs should be in one place, and any changes to them should reflect in all corners of Qiskit.
I am thinking of doing some changes to qiskit.circuit.library.standard_gates.__init__.py
but that should be out of scope of this PR.
Now, regarding that 1 failing test, If I do not use get_standard_gate_name_mapping
and do it in the old way, it passes.
But, If I use get_standard_gate_name_mapping
and change instr.param, or even if do instr.__init__(*args)
the test fails, it used to pass couple of months ago, but I don't know why when qpy loads the circuit the parameters changes somehow, and that fails the test.
@eliarbel , expecting your valuable comments :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking a bit at the failure, I'm afraid that your instruction-based implementation doesn't play well here with the singleton and caching models implemented for circuit data in the Rust space. In particular, the parameter updates of mutable instructions like here:
qiskit/qiskit/circuit/random/utils.py
Line 620 in 7cb79aa
current_instr.params = [int(parameters[p_start:p_end][0])] |
QuantumCircuit
's Rust data structure, hence the difference.
I think the best path forward is to resort to the implementation paradigm we had before in random_circuit
, i.e. with the Gate
classes lists. Practically, I would revert the changed in random_circuit
and change the implementation in random_circuit_from_graph
to follow a similar approach. In general BTW I liked your idea of pulling the standard gate lists from one place (as with get_standard_gate_name_mapping
), but this is something we can address in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eliarbel , done with the changes, Please, see if the PR is good enough now :)
@sbrandhsn , I humbly request you to have a look at this one, only if you have time to do so :) |
@sbrandhsn , I humbly request you to have a look at this one, only if you have time to do so :) |
@sbrandhsn , @1ucian0 , things are being converted to rust these days, is that the reason the PR is on hold? If, yes should I go ahead and port it to rust? I will correct these errors if this PR is required anymore :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @MozammilQ for your patience regarding a follow-up review.
Overall this seems in line with the spec in #12364. There are some changes required before it can be merged, please see my line comments.
Also:
- Regarding testing: I'm missing some validation related to the probabilities aspect. Can you come up with a test(s) (possibly using a fixed seed) to make sure 2Q gate distribution is within a reasonable expectancy w.r.t to the input probabilities?
- Performance: for very large circuits, how is the
random_circuit_from_graph
performance compared torandom_circuit
?
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/Added-random-circuit-from-graph-95c22eeabdea89d0.yaml
Outdated
Show resolved
Hide resolved
|
||
# This loop will keep on applying gates to qubits until every qubit-pair | ||
# has 2Q operations applied at-least `min_2q_gate_per_edge` times. | ||
while not all(np.array(list(edges_used.values())) >= min_2q_gate_per_edge): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering, do we want to exclude an edge from being assigned a random 2q gate once the min_2q_gate_per_edge
constraint is satisfied for it? Maybe as an option? Not sure what is the required behavior here, so this can be ignored.
@eliarbel , I have done with the changes, please do have a look :)
The test has been added in 6e7ede8 Below is the performance comparison :) from qiskit.circuit.random import random_circuit_from_graph, random_circuit
import numpy as np
import rustworkx as rx
import time
num_qubits = 5
h_h_g = rx.generators.directed_heavy_hex_graph(d=num_qubits, bidirectional = False)
seed = 32434
rng = np.random.default_rng(seed = seed)
cp_map_list = []
edge_list = h_h_g.edge_list()
# generating a non-normalized list of integers.
random_probs = rng.choice(range(10, 25), size = len(edge_list)).tolist()
sum_probs = sum(random_probs)
for idx, qubits in enumerate(edge_list):
ctrl, trgt = qubits
cp_map_list.append((ctrl, trgt, random_probs[idx]))
h_h_g.clear_edges()
h_h_g.add_edges_from(cp_map_list)
start_time = time.time()
qc = random_circuit_from_graph(h_h_g,
min_2q_gate_per_edge = 262,
max_operands = 2,
measure = True,
conditional = True, # Just making it a bit more challenging.
reset = True,
seed = seed,
insert_1q_oper = True,
prob_conditional = 0.91,
prob_reset = 0.50)
end_time = time.time()
count_oper = sum(qc.count_ops().values())
print(f"For `random_circuit_from_graph`:\nTime taken: {end_time - start_time} to generate {count_oper} operations")
print(f"Time taken per operation: {(end_time-start_time)/count_oper*1000000} us\n", 40*'#', '\n')
rand_start_time = time.time()
qc_rand = random_circuit(num_qubits = 166, depth = 1200, max_operands = 2, measure = True, conditional = True, reset = True, seed = seed)
rand_end_time = time.time()
count_rand_oper = sum(qc_rand.count_ops().values())
print(f"For `random_circuit`:\nTime taken: {rand_end_time-rand_start_time} to generate {count_rand_oper} operations")
print(f"Time taken per operation: {(rand_end_time-rand_start_time)/count_rand_oper*1000000} us") Output
|
Summary
The function
qiskit.circuit.random.utils.random_circuit_from_graph
adds a new featureof generating random circuits with interaction graph.
Details and comments