QTensor
QTensor copied to clipboard
Qubo simulations
<Recreating an older PR. This time the source branch is directly based on the upstream branch (both qubo_simulations).>
This PR extends QTensor to perform QAOA for general Ising Hamiltonians, which can contain constant, linear, and quadratic terms.
The Ising Hamiltonian is to be encoded as a networkx graph. For a Hamiltonian H = \sum_{i ≠ j} (A_ij/2) s_i s_j + \sum_i B_i s_i + C
The nodes of the graph correspond to the variables s_i = -1 or 1. A_ij is given by the 'weight' attribute of the edge between i and j. If an edge is absent, or doesn't have a 'weight', the corresponding A_ij is assumed to be 0. B_i is given by the 'weight' attribute of the node i. If the 'weight' attribute is absent for a node, the corresponding B_i is assumed to be 0. C is specified by a graph attribute called 'offset'.
Also included are
- Utilities to convent
- a networkx graph representation to DOcplex model and back. The DOcplex model uses binary variables (x_i=0 or 1).
- a networkx graph representation to qiskit QuadraticProgram and back. The qiskit QuadraticProgram uses binary variables (x_i=0 or 1).
- Tests to verify the working of the new features
- A few bugfixes
I've also added some package requirements: scipy, docplex, qiskit[optimization]. The first two are part of the dependency-tree of qtensor anyway. scipy is used to validate the working of the qubo simulations for small problems, and docplex and qiskit_optimization are used by the graph conversion utilities.
@danlkv Sorry about the new PR. I'm copying your message from the other thread here.
Hi @prasanthcakewalk, thanks for the contribution! I added a qubo_simulations branch, which is a dev with merged changes from master.
TODO, as discussed:
- Add tests, compare to qiskit or a well-known result
- Better handling of single-node energy
Problems to think about:
- pynauty fails to install. @prasanthcakewalk Could you provide more info on the issue? @rsln-s did you face any problems with pynauty python package? Maybe we should consider removing from setup.py dependencies and make it an optional dependency.
- A lot of subclasses to support different builders and composers. Consider changing the architecture.
Updates on the TODO items:
- I've added a test file which includes a naive scipy based QAOA simulator. The tests compare the results from qtensor and scipy for small problems.
- I've changed the handling of single-node energy by creating "node versions" of
_edge_energy_circuit,energy_expectation_lightcone,get_edge_subgraph, andenergy_expectation. This currently leads to some code repetition. It might be easier to modify the implementation of these functions to handle both nodes and edges. But that would be a major change to the current codebase, so we can leave that for after the approval of the new functionality.
Updates/thoughts on the other problems you mentioned:
- The reason
pynautydoesn't work for me is because it's a Unix/Linux/Mac only package. I'm on windows (and resisting using WSL). For now, I'm getting around it by just commenting out that requirement when installing QTensor in my virtual env. If it comes down to it, I can install WSL and usepynauty. - One way of getting around having different (sub)classes for the different builder-composer combinations would be have a metafunction/metaclass that takes the composer and builder as string valued inputs and returns the appropriate composer. This might be easier to use (and document). Here's ugly implementation that can be directly dropped into the current code:
def meta_composer(G, *args, composer='default', builder='qtree', **kwargs):
composer_dict = {
'default': QAOAComposer,
'old': OldQAOAComposer,
'zz': ZZQAOAComposer,
'weightedzz': WeightedZZQAOAComposer
}
builder_dict = {
'cirq': CirqBuilder,
'qiskit': QiskitBuilder,
'qtree': QtreeBuilder,
'qtreefull': QtreeFullBuilder,
'torch': TorchBuilder
}
unallowed_combinations = set()
if (composer, builder) in unallowed_combinations:
raise ValueError(f"The combination {composer = }, {builder = } is not allowed!")
class MetaComposer(composer_dict[composer]):
def _get_builder_class(self):
return builder_dict[builder]
return MetaComposer(G, *args, **kwargs)
A cleaner implementation would require some modifications, but might ultimately be worth it.
I think what's better is that the builder should be an argument of composer. For example:
qtensor.DefaultQAOAComposer(G, gamma, beta, composer='qtree')
The composer type could be a string identifier, or a builder object. By default we should use qtree. If it's a string identifier, use the builder_dict you made in the above comment. I don't think there are any unallowed combinations.
This ensures there's a "has-a" relationship rather than a "is-a". I think from software architecture this is called a Bridge pattern (an interesting read).
So, in UML that would be
I agree that the builder would be better as a property of the composer, passed during initialization.
Regarding the composers themselves, there may be a better way to organize them. It seems like the differences between OldQAOAComposer, QAOAComposer, and QAOAComposerChords are relatively more fundamental level than the other composer classes. For example, the ZZ versions differ from the non-ZZ versions only in the implementation of append_zz_term. This can potentially be passed as a string argument which sets the append_zz_term method from a set of possible functions.
Likewise, WeightedZZQAOAComposer differs from the unweighted composer in the problem that it solves. I wonder if it would make sense to start decouple the particular problem being solved from the QAOAsimulation. For example, you can start with a base class that solves a general Ising hamiltonian (with linear and quadratic terms) and returns the energy (with a dummy _post_process_energy that simply passes along the energy). Unweighted MaxCut can then implemented as a subclass of this base, with default edge weights set to 1, and an appropriately overridden _post_process_energy. I don't think the performance will be affected appreciably by this.
I'll see if I can put together a diagram (or a skeleton code) for these things
There are 3 tasks here:
- Complete the current implementation of generic ising, fast and well-defined, just some code-style fixes
- Refactor Composers to contain a Builder, using bridge pattern. Slower and requires some research.
- Refactor QAOA simulators and composers, decouple Simulation from the flavour of QAOA. I think this has several sub-tasks and requires even more research and planning.
I think 1 should be completed first, then 2. If you are interested in doing so we can go for 3, and the practice from doing 2 will help.
For diagrams, I recommend plantuml. There are several types of UML diagrams and the relevant for our case is Class diagram.
For example, this is the picture that I had in mind for solving 3.

You can edit it here http://www.plantuml.com/plantuml/uml/XP11ImCn48Nlyok6FUaXVw1uQFKiTH6zohGxxGuacJAPfIh-U3U5fWiUX1oIzvZlvSswE9bFAKBaWwChcXn7nq6CzowQOH-f57md4wflYY-ckyW9_aguGZsv9FdcZnCPfN9t3m1Nz-4dNE0X_8jh0w6Dz9ljfdzxt49MPxpyLWzDHUXyb8BpEOziZ42s4pUoht3cYk01SAGaqJKBPJmgbmuu3rY1q40rT9dvJ9zFazzQX5b_bJH5ShKLTbTzAbnkHWVjkkpTkp4t1zPpVDtzBm00
I came across the concept of multiple dispatch recently. It's when different versions of a function can be defined for different types of arguments. It's kinda similar to function overloading in C++, except overloading is based on static types. It's more similar to Python's class-based oop, where depending on the type of the object, you can have different definitions for the same operation. Except, instead of doing this kind of dispatch based only on the first argument (self), in multiple dispatch you can do it for all arguments.
I bring this up because, I think multiple dispatch can come in handy for refactoring QTensor. Apparently python has a few different packages which provide multiple dispatch support (eg: https://github.com/wesselb/plum). Maybe worth looking into?