matt-lourens / hierarqcal

Generate hierarchical quantum circuits for Neural Architecture Search.
https://matt-lourens.github.io/hierarqcal/
BSD 3-Clause "New" or "Revised" License
41 stars 15 forks source link

Build simple circuits with strings #28

Closed matt-lourens closed 1 year ago

matt-lourens commented 1 year ago

Summary

The idea is to allow common simple 1 and 2 qubit circuits to be built with a string passed to Qunitary. This would allow a for an elegant interaction to build complicated circuits from simple building blocks. For example:

Qinit(2)+Qcycle(mapping=Qunitary(f"CRX(x0)^01,CRZ(x1)^10)"))

simple_cycle Where currently this is achieved as follows:

def anz1(bits, symbols=None, circuit=None):
    # Assume bits are strings and in the correct QASM format
    q0, q1 = QuantumRegister(1, bits[0]), QuantumRegister(1, bits[1])
    circuit.crx(symbols[0], q0, q1)
    circuit.crz(symbols[1], q1, q0)
    return circuit
u = Qunitary(anz1, 2, 2)
hierq = Qinit(5) + Qcycle(mapping=u)

Qunitary can receive any function, and we want to allow it to receive a string that specifies a simple circuit. Emphasis on simple, the reason for this feature is to enable building small circuits quickly without needing to create a function, we don't need to cater for all possible use cases, just the commonly used gates. The string format I have in mind is:

"{gate_string}(parameters)^{bits},{gate_string}(parameters)^{bits},...,{gate_string}(parameters)^{bits}"

Where gate_string is one of: h,x,y,z,cx,cy,cz,rx,ry,rz,crx,cry,crz,s,tof,fred and always in either upper case or lower case. Then each gates {parameters} are specified in brackets (x1,x2,...,xn) separated with commas and n<10 (to avoid dis ambiguity and keep it simple. Then ^ specifies bits (the qubit indices that the gate acts upon). We should only allow up to 0-9 since most of the time only 2 qubit circuits are going to be created. The arity and number of symbols can then be determined by the distinct number of bits used and the total number of parameters specified, this should happen when Qunitary is initialised.

The conversion needs to happen when the Qhierarchy object is called (depending on the backend it executes get_circuit_qiskit for example). We'll need some mapping for the gate strings and the corresponding gate name for the backend used (we can stick with qiskit for now). Then some function needs to be constructed, I envisage something along the lines of:

def get_circuit_from_string(qunitary):
    def circuit_fn(bits, symbols=None, circuit=None):        
        arity=qunitary.arity
        qubits = [QuantumRegister(1, bits[i]) for i in range(arity)]
        s_idx = 0
        for gate_name, sub_bits, n_subsymbols in get_gateinfo_from_string(qunitary.circuit_string, arity, n_symbols):
            gate = getattr(circuit, gate_name)
            gate(*symbols[s_idx:n_subsymbols], *qubits.get(sub_bits))
        return circuit
    return circuit_fn

But that's just a guess, in particular I don't know how to handle the sub_bits.

For this issue, you can pick any framework (qiskit, pennylane, cirq) and implement the functionality for one of them. Feel free to come up with a better design, the above is just an idea for how to allow simple string circuits to Qunitary.

khnikhil commented 1 year ago

sounds fun! currently working on this one :)

matt-lourens commented 1 year ago

sounds fun! currently working on this one :)

Awesome @khnikhil! shout if anything is unclear, I'm happy to elaborate more on the issue or package.

khnikhil commented 1 year ago

I did have one quick question: could you explain your rationale behind applying the gates with gate(*symbols[s_idx:n_subsymbols], *qubits.get(sub_bits))?

This line seems like it would throw an error if n_subsymbols > 1 (since the gates you mention only take one gate parameter). If you assign each gate a new symbol/parameter, I could see how this would work, but in the case where you want to make two gates with the same rotation angle, this seems problematic. Additionally, when I've been debugging the code, I noticed that the symbols variable seems to be blank when I run something like the following.

hierq = Qinit(3)+Qcycle(mapping=Qunitary(f"h()^0;CRX(x_0)^01;CRZ(x_1)^10")) circuit = hierq(backend="qiskit")

Thus, I'm a bit confused as to how the symbols get generated. I looked through core.py, but couldn't find the answer.

For some background, my approach was to take the user-generated parameters (x1,x2,...xn) and pass them into the circuit_fn() function after taking the input string and partitioning it into individual gate instructions.

I've gotten this code to work in the context of making a 2-qubit circuit: hierq = Qinit(2)+Qcycle(mapping=Qunitary(f"h()^0;CRX(x_0)^01;CRZ(x_1)^10")) circuit = hierq(backend="qiskit") circuit.draw('mpl')

However, when I do 3+ qubits, I get a Qiskit error with the gate() function saying CircuitError: Name conflict on adding parameter XX, which seems to be overwriting the previous symbolic gate parameter (I'm using the qiskit.circuit.parameter object for the gate parameters).

This is a bit long-winded, but would love to hear your thoughts on

  1. Why you wrote the gate(*symbols[s_idx:n_subsymbols...) line
  2. How symbols are generated
  3. My approach above!

Thanks so much :)

Screen Shot 2023-05-26 at 2 25 29 AM
khnikhil commented 1 year ago

@matt-lourens please ignore the above, I looked more closely at the source code and figured out what I was doing wrong. about to send over a PR with the corrected code

matt-lourens commented 1 year ago

Hey @khnikhil, awesome for getting a PR out so quick!

I'll have time later today to check the PR out, excited to see to it!

andresulea commented 1 year ago

Can I work on this project ?

khnikhil commented 1 year ago

Can I work on this project ?

hi @andresulea, this one is almost done. I don't think there is a ton left to do, but if you're interested in working on hierarqcal it looks like two of the other issues don't have PRs yet, you might be able to join those teams.