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

Enhanced gates.Unitary draw with matplotlib #1618

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

sergiomtzlosa
Copy link
Contributor

Checklist:

  • Reviewers confirm new code works as expected.
  • Tests are passing.
  • Coverage does not decrease.
  • Documentation is updated.

Now for the circuit below:

c = models.Circuit(6)

c.add(gates.Unitary(np.random.random((8, 8)), 0, 2, 3))
c.add(gates.Unitary(np.random.random((2, 2)), 2))
c.add(gates.Unitary(np.random.random((2, 2)), 4))

plot_circuit(c)

The output image is:

imagen

@renatomello renatomello linked an issue Mar 21, 2025 that may be closed by this pull request
@renatomello renatomello added this to the Qibo 0.2.18 milestone Mar 21, 2025
@renatomello renatomello added enhancement New feature or request run-workflow Forces github workflow execution labels Mar 21, 2025
Copy link

codecov bot commented Mar 21, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.07%. Comparing base (0154fe4) to head (fe4c5e7).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1618   +/-   ##
=======================================
  Coverage   99.07%   99.07%           
=======================================
  Files          77       77           
  Lines       11437    11437           
=======================================
  Hits        11331    11331           
  Misses        106      106           
Flag Coverage Δ
unittests 99.07% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@renatomello renatomello changed the title enhanced unitaries draw with matplotlib Enhanced gates.Unitary draw with matplotlib Mar 21, 2025
@renatomello
Copy link
Contributor

Is there any way of distinguishing these unitaries from each other? For instance, the second and third unitaries in your example are different matrices but have the same symbol, so it might lead someone to think they are the same.

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 21, 2025

Is there any way of distinguishing these unitaries from each other? For instance, the second and third unitaries in your example are different matrices but have the same symbol, so it might lead someone to think they are the same.

These are the properties from the U-gates you mention:

{'name': 'Unitary',
 'draw_label': 'U',
 'is_controlled_by': False,
 'init_args': [array([[0.59587452, 0.61818819, 0.41597643, 0.90393119, 0.06621149,
          0.72290538, 0.77908855, 0.83662002],
         [0.42430393, 0.61242686, 0.88895057, 0.88425884, 0.52700795,
          0.90525265, 0.32402481, 0.41618417],
         [0.45058784, 0.06722899, 0.80816688, 0.66207815, 0.52801049,
          0.68084687, 0.42146987, 0.54617903],
         [0.86071525, 0.80881038, 0.15703967, 0.45291621, 0.04587542,
          0.81400596, 0.54256902, 0.32047022],
         [0.20801591, 0.69555816, 0.57814184, 0.21689202, 0.85507719,
          0.99327673, 0.84065296, 0.67474639],
         [0.83281928, 0.30620483, 0.06071928, 0.83475402, 0.37167302,
          0.43024746, 0.5071005 , 0.47252334],
         [0.27345507, 0.89303954, 0.68043481, 0.03191491, 0.68112454,
          0.52691193, 0.50706933, 0.19426751],
         [0.30004403, 0.1706441 , 0.8723928 , 0.47353485, 0.96016128,
          0.45373964, 0.59661599, 0.29120244]]),
  0,
  2,
  3],
 'init_kwargs': {'name': None, 'check_unitary': True, 'trainable': True},
 'unitary': False,
 '_target_qubits': (0, 2, 3),
 '_control_qubits': (),
 '_parameters': (array([[0.59587452, 0.61818819, 0.41597643, 0.90393119, 0.06621149,
          0.72290538, 0.77908855, 0.83662002],
         [0.42430393, 0.61242686, 0.88895057, 0.88425884, 0.52700795,
          0.90525265, 0.32402481, 0.41618417],
         [0.45058784, 0.06722899, 0.80816688, 0.66207815, 0.52801049,
          0.68084687, 0.42146987, 0.54617903],
         [0.86071525, 0.80881038, 0.15703967, 0.45291621, 0.04587542,
          0.81400596, 0.54256902, 0.32047022],
         [0.20801591, 0.69555816, 0.57814184, 0.21689202, 0.85507719,
          0.99327673, 0.84065296, 0.67474639],
         [0.83281928, 0.30620483, 0.06071928, 0.83475402, 0.37167302,
          0.43024746, 0.5071005 , 0.47252334],
         [0.27345507, 0.89303954, 0.68043481, 0.03191491, 0.68112454,
          0.52691193, 0.50706933, 0.19426751],
         [0.30004403, 0.1706441 , 0.8723928 , 0.47353485, 0.96016128,
          0.45373964, 0.59661599, 0.29120244]]),),
 'symbolic_parameters': {},
 'device_gates': set(),
 'original_gate': None,
 'parameter_names': 'u',
 'nparams': 64,
 'trainable': True,
 '_clifford': False}

I think that qibo does not provide enough information to distisguish the behaviour from each U-gate. Your comment is related to this very old issue: #478 (comment)

IMHO, It is not possible yet...or am I wrong?

@alecandido
Copy link
Member

alecandido commented Mar 21, 2025

Not that is simple, but in principle you could hash the matrix values in the gate parameters (hash(array) won't work, but you can hash its data buffer), and keep a global register as a dictionary (i.e. global to the single drawing, but local to the drawing function).

Every time you need to grab a symbol, you could just query the register. If it's in there, you take the index, if it's not, you add a new entry, with the d[hash] = len(d). Then, you could use the index as $U_{42}$ (for the hypothetical 42nd independent unitary in the drawing)

@sergiomtzlosa
Copy link
Contributor Author

Not that is simple, but in principle you could hash the matrix values in the gate parameters (hash(array) won't work, but you can hash its data buffer), and keep a global register as a dictionary (i.e. global to the single drawing, but local to the drawing function).

Every time you need to grab a symbol, you could just query the register. If it's in there, you take the index, if it's not, you add a new entry, with the d[hash] = len(d). Then, you could use the index as U 42 (for the hypothetical 42nd independent unitary in the drawing)

Thanks @alecandido , but I do not understand how to pick up the data buffer and check on every U-gate, do you mean to hash the full U-gate? I do not catch how to pick up each single U-gate, can you point me a little example please?

@renatomello
Copy link
Contributor

renatomello commented Mar 21, 2025

@sergiomtzlosa the gates.Unitary class has a name attribute. I guess we could think of a mechanism that uses name to distinguish between unitaries.

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 21, 2025

thanks a lot @alecandido , the property name holds the same name for the three single unitaries on three-unitary gate:

gates.Unitary(np.random.random((8, 8)), 0, 2, 3)

Should we think about add another property on the Unitary class to set a name to each single Unitary gate? What do you think?

@renatomello
Copy link
Contributor

renatomello commented Mar 21, 2025

Should we think about add another property on the Unitary class to set a name to each single Unitary gate? What do you think?

My first inclination is to say that the change needs to be in the Circuit.add method. For instance, there could be a hidden integer number in the Circuit object that keeps track of how many gates.Unitary were added to the circuit and if one is added without a custom name, then the gate name becomes something like $U_{number}$ and then number += 1.

This is just one idea, but we could think of others.

@alecandido
Copy link
Member

alecandido commented Mar 21, 2025

@sergiomtzlosa

[ins] In [3]: u = gates.Unitary(np.ones((2,2)), 0)
[ins] In [4]: hash(u.parameters[0].data.tobytes())
Out[4]: 6949360754640926774

No need to add any further info (if you accept to name them $\{U_i\}_i$).

@renatomello
Copy link
Contributor

@sergiomtzlosa

[ins] In [3]: u = gates.Unitary(np.ones((2,2)), 0)
[ins] In [4]: hash(u.parameters[0].data.tobytes())
Out[4]: 6949360754640926774

No need to add any further info (if you accept to name them { U i } i ).

A hash is a unique identifier but it isn't user-friendly, especially for plotting.

@alecandido
Copy link
Member

alecandido commented Mar 21, 2025

A hash is a unique identifier but it isn't user-friendly, especially for plotting.

I was not proposing to use a hash as the matrix index, but to use the hash to build a register:

def unitary_id(u: Unitary, register: dict) -> int:
    h = hash(u.parameters[0].data.tobytes())
    id_ = register.get(h)
    if id_ is not None:
    	return id_
    id_ = register[h] = len(register)
    return id_


register: dict[int, int] = {}
id_ = unitary_id(u, register)

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 21, 2025

thanks @renatomello and @alecandido !! both solutions are good!!👍. I think that we can simplify the @alecandido solution by getting a global hash for the Unitary gate to identify it and then label each single U-gate with an index according to the number of target qubits, from 0 to len(_target_qubits) just for drawing....I keep on thinking a solution.

@alecandido
Copy link
Member

alecandido commented Mar 21, 2025

Sorry @sergiomtzlosa, but anything "global" is only simple for a relative short time span. As soon as you will have to maintain (change, debug it, or debug something related) it would get much more complicate. Also because it will be more complicate to reproduce.

I do not want to argue that the solution I proposed is the unique and best one. Just be careful about global objects (including having classes so huge which will act almost as a global context for most executions, as it could be a Circuit itself, if the whole application consists in its evaluation).

@sergiomtzlosa
Copy link
Contributor Author

thanks @alecandido , sorry for that, I will focus on a more flexible solution like yours😉

@alecandido
Copy link
Member

thanks @alecandido , sorry for that, I will focus on a more flexible solution like yours😉

Nothing to be sorry about. And despite being rather direct, I just wanted to be clear about something I consider relevant. But it was absolutely not meant to criticize your approach.

I know the pros and cons of working with global objects myself (and, by now, I know there are mostly cons...), just because I went through the same issues myself. So, it was just me sharing a bit of hard-learnt experience :P

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 21, 2025

great @alecandido ! I did not feel criticised at any time. I truly understand your point of view and as you know how qibo works internally more than me, I feel very happy to be part of the discussion, and as you have more expertise than me on qibo and I will follow your ideas to do a better framework.

Thanks a lot!

@renatomello
Copy link
Contributor

@sergiomtzlosa what does the left subscript mean?

@sergiomtzlosa
Copy link
Contributor Author

hi @renatomello ! I set the left subscript just to enumerate the gates, that'a all! they do nothing

@renatomello
Copy link
Contributor

hi @renatomello ! I set the left subscript just to enumerate the gates, that'a all! they do nothing

I thought the right subscripts were enumerating the unitaries...

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 27, 2025

as they do nothing on both sides, then I should remove them...

@alecandido
Copy link
Member

@renatomello I actually suggested it above:

  • the right ones are enumerating the gates, as you originally proposed
  • the left ones are enumerating the qubits involved in the gates

This is to distinguish CNOT(1,3) from CNOT(3,1), when CNOT is represented as a unitary (i.e. to explicitly identify the action of unitary which is not symmetric under qubits swaps).
In any case, it is just a refinement for unitaries used multiple times on different sets of qubits.

@sergiomtzlosa
Copy link
Contributor Author

I do not know how to do that because this Unitary gates representation do not distinguish between target and control qubits...can you point me how to achive this please?🙏

@renatomello
Copy link
Contributor

renatomello commented Mar 27, 2025

@renatomello I actually suggested it above:

* the right ones are enumerating the gates, as you originally proposed

* the left ones are enumerating the qubits involved in the gates

This is to distinguish CNOT(1,3) from CNOT(3,1), when CNOT is represented as a unitary (i.e. to explicitly identify the action of unitary which is not symmetric under qubits swaps). In any case, it is just a refinement for unitaries used multiple times on different sets of qubits.

For that, I would suggest a superscript like $U_{1}^{(1,3)}$ (or the reverse order i.e. $U_{1,3}^{(1)}$) since it is much more common and natural than a left subscript.

@alecandido
Copy link
Member

I do not know how to do that because this Unitary gates representation do not distinguish between target and control qubits...can you point me how to achive this please?🙏

I expected it to be exactly what you did. In the plot in #1618 (comment), I expected that, if you reused twice $U_0$, the same $U_0$ symbol would be used.

So, if a circuit contains

c.add(Unitary(array, 0, 1, 3))
c.add(Unitary(array, 2, 1, 3))

both gates would be represented with 3 boxes using ${}_0 U_0$, ${}_0 U_1$, and ${}_0 U_2$. But they will be sorted accordingly to the qubit order, with ${}_0 U_0$ placed on the first wire in the former case, and on the third wire on the latter (while the other two boxes will be on the same wires for both gates).

@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Mar 27, 2025

I do not know how to do that because this Unitary gates representation do not distinguish between target and control qubits...can you point me how to achive this please?🙏

I expected it to be exactly what you did. In the plot in #1618 (comment), I expected that, if you reused twice U 0 , the same U 0 symbol would be used.

So, if a circuit contains

c.add(Unitary(array, 0, 1, 3))
c.add(Unitary(array, 2, 1, 3))

both gates would be represented with 3 boxes using 0 U 0 , 0 U 1 , and 0 U 2 . But they will be sorted accordingly to the qubit order, with 0 U 0 placed on the first wire in the former case, and on the third wire on the latter (while the other two boxes will be on the same wires for both gates).

I have a question, the array is the same for both gates? Maybe what you explained is already done on the left subscript:

imagen

sorry for bothering you....

@alecandido
Copy link
Member

Yes, that's correct.

But apparently the right subscript it is not: if the array is the same (what I implied using twice the array identifier in the example above), the right subscript should have been the same as well.

@sergiomtzlosa
Copy link
Contributor Author

Finally, by having a look at the example below:

import numpy as np
from qibo import Circuit, gates, models, construct_backend
from qibo.ui import plot_circuit
from qibo.quantum_info import random_unitary

backend = construct_backend("numpy")

c = models.Circuit(4)

array1 = random_unitary(8, backend=backend, seed=42)
array2 = random_unitary(8, backend=backend, seed=44)
array3 = random_unitary(8, backend=backend, seed=48)

c.add(gates.Unitary(array1, 0, 1, 3))
c.add(gates.X(0))
c.add(gates.Unitary(array1, 1, 0, 3))
c.add(gates.Unitary(random_unitary(2, backend=backend, seed=50), 2))
c.add(gates.Unitary(array2, 1, 0, 3))
c.add(gates.Z(1))
c.add(gates.Unitary(array2, 2, 1, 0))
c.add(gates.Y(2))
c.add(gates.Unitary(array3, 1, 0, 3))

plot_circuit(c);

The drawer will show as:

imagen

@renatomello renatomello requested a review from alecandido April 2, 2025 10:15
@sergiomtzlosa
Copy link
Contributor Author

sergiomtzlosa commented Apr 4, 2025

I reviewed the merge conflicts with the master branch, you can have a look for merging. Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request run-workflow Forces github workflow execution
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Global unitaries support on plot_circuit Enhance unitary gates drawing in
3 participants