Skip to content
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

Create glorious_folding_amplifier.py #36

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

mrvee-qC-bee
Copy link

@mrvee-qC-bee mrvee-qC-bee commented Jan 24, 2023

refactor(folding_amplifier): init dag folding amplifier

Summary

Details and comments

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Great job @mrvee-qC-bee! 🥳

I have left some comments, once you're done with them try changing copies for num_foldings and appending two circuits per num_folding instead of just one as you are doing with copies. You should always retain the original circuit too, so this should give you something like:

  • num_foldings=0 → output circuit is the same as input circuit (i.e. once)
  • num_foldings=1 → output circuit is input circuit repeated three times
  • num_foldings=2 → output circuit is input circuit repeated five times

Remember to keep inserting barriers between each copy! If you feel brave enough, try to invert the first of those two circuits that you append afterwards 😉

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

This is very good! Congrats!

Some general comments:

  1. Try to do one thing only per commit (i.e. avoid something: this + that)
  2. Try to write the scope in the commit message (e.g. refactor(folding_amplifier): update ...)

Other than that, great job! Try to act on the new comments and let's pick it up from there 🥳

Comment on lines 24 to 27
def _inverse_dag(dag_to_inverse: DAGCircuit) -> DAGCircuit:
"""Inverts dag circuit"""
dag_to_inverse = dag_to_circuit(dag_to_inverse).inverse()
return circuit_to_dag(dag_to_inverse)
Copy link
Member

@pedrorrivero pedrorrivero Jan 29, 2023

Choose a reason for hiding this comment

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

This is a good utility function! I am surprised that this does not exist natively in terra. We may want to contribute it (there is a similar method called DAGCircuit.reverse with an implementation just like yours).

Before doing that think about the following question: How many times do you traverse the entire circuit with this implementation? (Hint: look instruction by instruction and add up)

Once you respond to that, try to respond to the same question but for a piece of code which traverses the DAGCircuit in reverse order (i.e. form OutNodes to InNodes) adding the inverse gates as it encounters them.

Which one is (roughly) more efficient? (i.e. traverses the circuit less times and therefore is faster)

No need to update the code, but something to be aware of 😉

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Good, good, good!! This is great!!

Try to keep all comments related to Terra contributions UNresolved so that we remember to do those at some point 🙂


def apply_folding(original_dag: DAGCircuit, num_foldings: int) -> DAGCircuit:
"""Build dag circuit by composing copies of an input dag circuit."""
noisy_dag = copy.deepcopy(original_dag)
Copy link
Member

@pedrorrivero pedrorrivero Jan 29, 2023

Choose a reason for hiding this comment

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

Again, greatly surprised that there is no native DAGCircuit.copy method. We may need to contribute that to Terra as well. 😮

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Interesting approach! See comments 🙂

Comment on lines 22 to 25
dag_to_inverse = dag_to_circuit(dag_to_inverse)
dag_to_inverse.barrier()
dag_to_inverse = dag_to_inverse.inverse()
dag_to_inverse.barrier()
Copy link
Member

Choose a reason for hiding this comment

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

Uhh, I had not though of this! Interesting!

You should refrain from this approach for several reasons, but probably the most important is that you are not doing what the function promises. Instead you are doing extra things (i.e. adding two barriers) that are not expected, this is sometimes referred to with the broader term side effect which good code tries to avoid at all cost, as it makes it harder to understand and predict.

How about something like:

barrier = Barrier(noisy_dag.num_qubits())
...
for _ in range(num_foldings):
    noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
    noisy_dag.compose(inverse_dag, inplace=True)
    ...

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Great job!! Some clean-up to do, and some puzzles to solve, but you are making progress sir! 🎈

Comment on lines 71 to 76
inverse_dag = self._invert_dag(dag)
for _ in range(num_foldings):
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(inverse_dag, inplace=True)
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(dag, inplace=True)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
inverse_dag = self._invert_dag(dag)
for _ in range(num_foldings):
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(inverse_dag, inplace=True)
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(dag, inplace=True)
inverse_dag = self._invert_dag(original_dag)
for _ in range(num_foldings):
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(inverse_dag, inplace=True)
noisy_dag.apply_operation_back(barrier, qargs=noisy_dag.qubits)
noisy_dag.compose(original_dag, inplace=True)

Copy link
Author

Choose a reason for hiding this comment

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

This seems to fail lint! The DAGNoiseAmplifier has dag as the variable name than original_dag :(

Comment on lines 54 to 60
def _apply_full_folding(
self,
noisy_dag: DAGCircuit,
dag: DAGCircuit,
barrier: Barrier,
num_foldings: int,
) -> DAGCircuit:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def _apply_full_folding(
self,
noisy_dag: DAGCircuit,
dag: DAGCircuit,
barrier: Barrier,
num_foldings: int,
) -> DAGCircuit:
def _apply_full_folding(
self,
noisy_dag: DAGCircuit,
original_dag: DAGCircuit,
barrier: Barrier,
num_foldings: int,
) -> DAGCircuit:

A few things to notice i this signature:

  1. As mentioned before, why do you need barrier as an arg? Are you going to be passing anything different any time?
  2. why do we pass noisy_dag? I know you drew inspiration from the other class, but do you understand the reasons behind this? If not, there is no need to rush it, let's take it one step at a time and re-discover these things as we go 🙂 Your code really does not require that you pass a noisy_dag, better to provide the orginal_dag and num_foldings and return a freshly created noisy_dag for now.
  3. Keep naming consistent, original_dag is better than just dag because it emphasizes the difference with noisy_dag

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

This is starting to look very professional!! 💪🏼

Comment on lines 53 to 58
inverted_dag = dag_to_inverse.copy_empty_like()
for node in dag_to_inverse.topological_op_nodes():
inverted_dag.apply_operation_front(
node.op.inverse(), qargs=node.qargs, cargs=node.cargs
)
return inverted_dag
Copy link
Member

Choose a reason for hiding this comment

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

SUPER COOL!! You wrote this using only the DAG API, which makes the code more efficient and, ready to be contributed to Terra!! Let's take care of this in our next chat 🥳

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Docstrings look good! Just a few comments, you are ready to move to the next step 🥳

Rename this class so it is clear this one is global and create a new class for the local amplifier! Avoid looking at the previous code, try to figure this one out on your own 😉

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Some details 🙂

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Good job!! 🎉

I left some remarks to get you thinking about more advanced topics on software design. Let me know what you think 😉

Comment on lines 71 to 73
noisy_dag = self._insert_folding(noisy_dag, node, num_foldings)
else:
noisy_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs)
Copy link
Member

Choose a reason for hiding this comment

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

How about we try to mimic the language that is already in place to make the code more readable? 🙃

Suggested change
noisy_dag = self._insert_folding(noisy_dag, node, num_foldings)
else:
noisy_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs)
noisy_dag = self._apply_folded_operation_back(noisy_dag, node, num_foldings)
else:
noisy_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs)

@pedrorrivero
Copy link
Member

There is one more thing that we could do here: add some configuration to the amplifiers that controls whether barriers are applied or not. 🙃

@pedrorrivero pedrorrivero added enhancement New non-feature request (e.g. performance) PL-3 Priority level 3/5 → Medium labels Mar 2, 2023
Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Great progress!! Let's discuss, and keep it up! 🥳


from ..noise_amplifier import DAGNoiseAmplifier

Folding = namedtuple("Folding", ("full", "partial", "effective_noise_factor"))
Copy link
Member

Choose a reason for hiding this comment

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

This is very good!

We should maybe rethink the language at some point according to that glossary that I sent you a while back, so that terminology is a bit clearer 🙂

Folding: named tuple containing full foldings, number of gates to partially fold and
effective noise factor of the operation
"""
noise_factor = self._validate_noise_factor(noise_factor)
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 an internal function you do not need to perform validation and it should be avoided unless strictly necessary. Instead, assume that the input is correct (after all it will be you passing it along) and move validation to the point where the actual user of the tool passes noise_factors only 🙂

Copy link
Author

Choose a reason for hiding this comment

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

Tried something on those lines! 🥳

)
return Folding(full, partial, effective_noise_factor)

def _compute_best_estimate(self, num_nodes: float, partial_foldings: float) -> float:
Copy link
Member

Choose a reason for hiding this comment

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

This name is not explanatory: if I am new to the tool and I read it, I would not know what is this a best estimate of!

Naming is tough!! 😅

Copy link
Author

Choose a reason for hiding this comment

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

Done! (sorta!) 🥳

if self.barriers:
barrier = Barrier(dag.num_qubits())
dag.apply_operation_back(barrier, qargs)

def _invert_dag(self, dag_to_inverse: DAGCircuit) -> DAGCircuit:
Copy link
Member

Choose a reason for hiding this comment

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

Simple and elegant function! I love it 😍

Copy link
Author

Choose a reason for hiding this comment

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

TODO: Contribute to terra

@@ -33,99 +32,74 @@ class GloriousLocalFoldingAmplifier(DAGNoiseAmplifier):
`<https://ieeexplore.ieee.org/document/9259940>`
"""

def __init__(self, gates_to_fold: Sequence[int | str], barriers: bool = True) -> None:
def __init__(self, gates_to_fold: Set[Union[str, int] | None], barriers: bool = True) -> None:
self.gates_to_fold = self._validate_gates_to_fold(gates_to_fold)
Copy link
Member

Choose a reason for hiding this comment

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

Validation should happen in the setter —you currently don't have one, add it! 👨🏼‍💻

Comment on lines 103 to 105
VALID_GATES = set( # pylint: disable=invalid-name
standard_gates.get_standard_gate_name_mapping().keys()
)
Copy link
Member

Choose a reason for hiding this comment

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

How do we handle custom gates? 🤔

Copy link
Author

Choose a reason for hiding this comment

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

Will ask a query in our next meet! :D

full_foldings = int(foldings)
partial_foldings = foldings - full_foldings

gates_to_partial_fold = self._compute_best_estimated_gates(num_nodes, partial_foldings)
Copy link
Author

Choose a reason for hiding this comment

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

Change to num_partial_folds

Copy link
Member

Choose a reason for hiding this comment

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

I have had an idea on how to make this even cleaner, let's discuss on Tuesday! 🙂 It is based on computing the total number of foldings to perform in the whole circuit directly, and choosing how that gets distributed later

@@ -31,27 +31,16 @@ class GloriousGlobalFoldingAmplifier(GloriousFoldingAmplifier):
`<https://ieeexplore.ieee.org/document/9259940>`
"""

def __init__(self, barriers: bool = True) -> None:
self._set_barriers(barriers)
def __init__(self) -> None:
Copy link
Author

Choose a reason for hiding this comment

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

Add barrier and tolerance

) -> None:
super().__init__()
self._set_custom_gates(custom_gates)
self._set_gates_to_fold(gates_to_fold)
Copy link
Author

Choose a reason for hiding this comment

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

Add barriers and tolerance

Copy link
Member

@pedrorrivero pedrorrivero left a comment

Choose a reason for hiding this comment

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

Good, let's push this through the finish line! :) The PR is getting quite substantial, it might be good to split it in smaller chunks. Let's consider this in our next chat!

################################################################################
@property
def barriers(self) -> bool:
"""Barriers setter"""
Copy link
Member

Choose a reason for hiding this comment

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

This is actually a getter not a setter, but it is also good if you explain what the property means (e.g. if set to True, barriers are added when folding) 🙂

return self._barriers

def _set_barriers(self, barriers: bool) -> None:
"""Set barriers property"""
Copy link
Member

Choose a reason for hiding this comment

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

This is a setter on the other hand!


@property
def tolerance(self) -> float:
"""Tolerance setter"""
Copy link
Member

Choose a reason for hiding this comment

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

Again, not a setter, but it is better to explain the property in this docstring. It is somewhat eveident tht this is a getter so writing it on the docstring is not adding any relevant information 🙃

return self._tolerance

def _set_tolerance(self, tolerance: float) -> None:
"""Set Tolerance property"""
Copy link
Member

Choose a reason for hiding this comment

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

This is the real tolerance setter

Suggested change
"""Set Tolerance property"""
"""Set tolerance property"""

full_foldings = int(foldings)
partial_foldings = foldings - full_foldings

gates_to_partial_fold = self._compute_best_estimated_gates(num_nodes, partial_foldings)
Copy link
Member

Choose a reason for hiding this comment

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

I have had an idea on how to make this even cleaner, let's discuss on Tuesday! 🙂 It is based on computing the total number of foldings to perform in the whole circuit directly, and choosing how that gets distributed later

################################################################################
## INTERFACE IMPLEMENTATION
################################################################################
def amplify_dag_noise(self, dag: DAGCircuit, noise_factor: float) -> DAGCircuit:
"""Applies global folding to input DAGCircuit and returns amplified circuit"""
noise_factor = self._validate_noise_factor(noise_factor)
Copy link
Member

Choose a reason for hiding this comment

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

GREAT! 👏🏼

Comment on lines +56 to +63
@property
def custom_gates(self) -> bool:
"""Custom_gates flag setter"""
return self._custom_gates

def _set_custom_gates(self, custom_gates: bool) -> None:
"""Set gates_to_fold property"""
self._custom_gates = bool(custom_gates)
Copy link
Member

Choose a reason for hiding this comment

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

What is this for? 🤔

Copy link
Author

Choose a reason for hiding this comment

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

TODO: Remove entirely

Comment on lines +122 to +124
if self.custom_gates:
pass
# TODO: Append valid custom_gate_names to VALID_GATES
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain this further? Thanks! 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New non-feature request (e.g. performance) PL-3 Priority level 3/5 → Medium
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants