Giter Club home page Giter Club logo

mqt-core's People

Contributors

33gjl1xe avatar aaronleesander avatar bertiflorea avatar burgholzer avatar dependabot[bot] avatar eliaslf avatar github-actions[bot] avatar hartwigb avatar hillmich avatar isfairy avatar joachimmarin avatar katringoogoo avatar martin-fink avatar ollpu avatar pehamtom avatar pichristoph avatar pre-commit-ci[bot] avatar rahimiparham avatar reb-ddm avatar tewas avatar tobiasprie avatar tyi1025 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

mqt-core's Issues

Support for OpenQASM 3.0 additional datatypes

The new OpenQASM standard introduces many additional datatypes besides quantum and classical registers. This allows to describe classical components of quantum algorithms in a very intuitive way. As a first step, support for all kinds of new types should be added to the QFR library and, correspondingly, to the QuantumComputation class. For details on all the new typed provided by the standard, see https://qiskit.github.io/openqasm/language/classical.html.

Similar to #29, the QFR should support Integers, Floating point numbers, Fixed-point angles, complex numbers, and boolean types.
The QuantumComputation class should be capable of allocating and maintaining these types. The ClassicalOperation class introduced in #29, will most likely need to be expanded to incorporate all the possible operations on these classical types.

Depends on #29.

Ancillary and garbage support for `.real` parser

At the moment, the .real file parser (see https://github.com/cda-tum/qfr/blob/main/src/parsers/RealParser.cpp) does not handle constant or garbage definitions properly.
This has not been particularly important up until now, since we typically just consider the whole state vector during simulation (e.g., in cda-tum/ddsim#main) and similarly the complete functionality for equivalence checking.
However, it might become important in the future for our endeavours towards reversible circuits.

In particular,

  • any constant should be properly flagged as an ancillary qubit in the resulting QuantumComputation.
  • any garbage should be properly flagged as a garbage qubit in the resulting QuantumComputation.
  • the inputs and outputs could be used to determine the initial_layout and the output_permutation respectively. Although, it is not so clear whether this is possible for any .real file.

✨ Support for Qiskit's `final_layout`

What's the problem this feature will solve?

When reasoning about quantum circuits there are two degrees of freedom that can always lead to problems/confusion:
An arbitrary permutation of a circuit's qubits might be applied at the very beginning of the circuit (referred to as an "initial layout") or at the end of the circuit (referred to as an "output permutation").
In almost any case, a circuit containing such permutations would still be considered equivalent to a circuit without such permutations.
This is especially relevant for verifying the equivalence of quantum circuits and the results of compilation flows in particular.

For a long time, Qiskit has supported a _layout property for its QuantumCircuit objects that captured the initial layout applied during compilation. Within the MQT we use this property to infer the initialLayout for our QuantumComputation objects.
In 0.22.0 (which is now our minimum supported version), Qiskit changed the _layout property to a proper TranspileLayout class.
0.23.0 introduced an additional final_layout component that (in some confusing fashion) captures the output permutation.
This should allow to also infer the output permutation from Qiskit.
Right now, we use the measurements in a circuit for that purpose, which is rathe prone to errors (e.g., someone not adding measurements to their circuit before compilation).

Describe the solution you'd like

Starting with 0.45.0. (which should be the next minor release), the ergonomics of the TranspileLayout class have been considerably improved (see Qiskit/qiskit#10835).
That should yield a blueprint for how to interpret the final_layout and to infer the outputPermutation from Qiskit circuits.

A potential solution would first look if the final_layout attribute is present.
If so, it would check whether the new convenient syntax (>=0.45.0) is available.
If not, it replicates the logic to parse the output permutation.
If no final layout is present, the solution should check for measurements in the circuit and infer the output permutation from there (as it currently does).

🐛 DD Node Leak

Environment information

Might date back to rather old versions of the DD package. Still trying to figure that out.

Description

The following simple example (that has been reduced from a much bigger real use case) consistently leaks decicion diagram nodes

#include "gtest/gtest.h"

#include "dd/Package.hpp"

using namespace qc;

TEST(DebugTest, debug) {
  const auto nqubits = 1U;
  auto matrix = dd::GateMatrix {
      dd::complex_one,
      dd::complex_zero,
      dd::complex_zero,
      dd::complex_zero
  };
  auto dd = std::make_unique<dd::Package<>>(nqubits);

  auto dd1 = dd->makeGateDD(matrix, nqubits, 0U);
  matrix[0] = dd::complex_zero;
  matrix[3] = dd::complex_one;
  auto dd2 = dd->makeGateDD(matrix, nqubits, 0U);
  const auto repetitions = 10U;
  for (auto i=0U; i<repetitions; ++i) {
    dd->multiply(dd1, dd2);
  }
  dd->garbageCollect(true);
  EXPECT_EQ(dd->mMemoryManager.getUsedCount(), 0U);
}

Since no reference counting is performed, every DD node should be reclaimed in the end.
However, with the current code base, every repetition of the main multiplication leaks a matrix DD node.

A simple example where computations as the above frequently arise is during the multiplication of CNOT gate DDs, e.g., CNOT(0, 1) * CNOT(0, 1).

After some painful digging, I found the error here:

// all equal to zero
if (argmax == -1) {
if (!cached && !e.isTerminal()) {
// If it is not a cached computation, the node has to be put back into
// the chain
getMemoryManager<Node>().returnEntry(e.p);
}
return Edge<Node>::zero;
}

If a cached node only has zero successors, then that part of the code will not free the node but happily return the zero terminal.
I still don't quite understand why that hasn't surfaced earlier and will dig into that a bit more.

Expected behavior

The DD package should not leak memory.

How to Reproduce

Execute the above test and watch it fail.

🔧 Disable IPO/LTO per default

In the current project configuration, link time optimization is enabled by default (to optimise performance).
This has lead to multiple cases where developers had issues during debugging, e.g., break points not being hit or code not being executed.

Consequently, IPO should be opt-in instead of opt-out.
Instead of enabling it by default, it can just be enabled in CI.

Use JSON_MultipleHeaders=OFF for nlohmann/json

A recent commit changed the default behaviour of nlohmann/json to use JSON_MultipleHeaders=ON instead of OFF.

This breaks at least the build from sdist for DDSIM (which prunes the multiple header folder). This is locally fixable by setting the option accordingly (or use the multiple headers).

Question: Should we set JSON_MultipleHeaders=OFF in the qfr via set(JSON_MultipleHeaders OFF CACHE INTERNAL "") at the appropriate location?

✨ Add release drafter workflow

Similar to our other repositories, a release drafter action should be set up here to ease the process of releasing new versions.

✨ Enhance ZX gate support with newly introduced gates

#280 has brought support for many new gates. The ZX library is yet to be updated to natively support these gates.
As such, circuits containing these operations cannot be translated to ZXDiagrams at the moment.

All that's required to fix this, is to provide the corresponding transformations in the ZX library.

Support for OpenQASM 3.0 classical bits and registers

The new OpenQASM standard introduces many additional datatypes besides quantum and classical registers. This allows to describe classical components of quantum algorithms in a very intuitive way. As a first step, support for all kinds of new types should be added to the QFR library and, correspondingly, to the QuantumComputation class. For details on all the new typed provided by the standard, see https://qiskit.github.io/openqasm/language/classical.html.

Classical bits and registers could previously just be used as the target of a measurement and (in a very limited fashion) as the classical control in an if statement. The new OpenQASM 3.0 standard enriches these capabilities by allowing to:

  • assign bitstrings to classical registers, e.g., bit[8] a = "10001111";
  • execute bitwise operations such as and (&), or |, xor ^ and the corresponding assignment operators &=, |=, ^=
  • left and right shift << and >>, as well as <<= and >>= by an unsigned integer
  • not ~
  • circular shifts rotl and rotr

First of all, the QuantumComputation class needs to be capable of allocating and maintaining classical registers.
Most certainly, the manipulation of these registers will require the introduction of a new kind of operation, e.g., ClassicalOperation.
Such an operation will not be unitary. As a consequence, the concept of the NonUnitaryOperation as it is now may need to be revisited/refactored.

Depends on #28 for the availability of the bit keyword in the parser.

📝 Better API Docs

What's the problem this feature will solve?

API Docs are hard to get working with proper docstrings, automatically and look nice. Two out of the three are easy, but all three seem hard.
The solution implemented in #371 works automatically and looks nice (e.g., with working inter-doc and inter-sphinx links). However, it is purely generated from the type stubs, which do not contain docstrings.
As such, all the effort that went into providing docstrings in the bindings code is not reflected in the documentation.

IDEs do pick up the documentation from the bindings module and so do other API doc tools that inspect the compiled module. However, we haven't managed to get the rendered API docs to look nice and inter-doc linking to work properly.
The problem boils down to mqt.core vs. mqt.core.core_ in the docstrings and how to make the docs only show the former and never refer to the latter.

One potential solution could be to have the docstrings in the stub files. However, this does not seem not be recommended as a ruff check removes them per default. That could be turned off though.
I also believe that IDEs do not pick up the documentation from stub files typically.
Having to duplicate the documentation (in bindings code and stubs) does not really make sense to me at all as it is not maintainable in the future.

Describe the solution you'd like

It should be possible to set up automatically built API documentation for compiled projects that looks nice and contains all the docstrings so that IDEs and the Docs can pick them up. We are not the first project to do something like this, but I tried for quite some ours myself and what we have right now is the best I could come up with.
Any help is appreciated.

Error when parsing Qiskit circuits with ancilla quibits

Running the following code

from qiskit import compiler, QuantumCircuit, QuantumRegister, ClassicalRegister, AncillaRegister
from jkq import qfr

flag = AncillaRegister(1, 'flag')
qc = QuantumCircuit(flag)
qc.x(flag)
qfr.construct(qc)

gives the following error message:

{'error': "Could not import circuit: KeyError: (AncillaQubit(AncillaRegister(1, 'flag'), 0),)"}

Support for OpenQASM 3.0 global phase gate

The OpenQASM 3.0 standard introduces a new type of gate gphase(γ) that can be used to add a global phase e^iγ to the scope containing the instruction.

This operation shall also be introduced as a new StandardOperation.

Technically, this instruction has no target (since it is a global phase). As such, introducing it in the StandardOperations class may require special treatment. Its decision diagram is given by the identity matrix where the top weight is changed to e^iγ. The inverse is given by the identity with top edge weight e^-iγ.

Support for OpenQASM 3.0 constant values

The new OpenQASM standard introduces many additional datatypes besides quantum and classical registers. This allows to describe classical components of quantum algorithms in a very intuitive way. As a first step, support for all kinds of new types should be added to the QFR library and, correspondingly, to the QuantumComputation class. For details on all the new typed provided by the standard, see https://qiskit.github.io/openqasm/language/classical.html.

It also introduces the concept of constant values that can be declared with the const modifier. These are essentially known at "compile-time" and can be used to describe mathematical expressions (similar to OpenQASM 2.0's parameter expressions).

The QuantumComputation class should be capable of managing constants and performing tasks such as constant propagation.

Depends on #30 for the availability of the new datatypes.

✨ Potentially refactor compute tables In DD package

What's the problem this feature will solve?

At the moment, each individual decision diagram operation has its own compute table. On the one hand, this is neat, since it allows to fine tune the sizes of the individual compute tables. On the other hand, it creates quite some complexity across the package. Reading the literature on BDD packages and looking at the history of this DD package, this is frequently handled differently: There is a single compute table and the operation being performed is factored into the hash during lookup.
For BDDs that is a no-brainer since there is only one type of operand so it is fairly efficient to cook up a compute table data structure for that. Typically, even unary operations are handled in the same table as binary operations.
In the very past, this was also possible (and implemented) for our DD package. This was due to there only being one type of node (vectors and matrices shared the same node type, but vectors only used two of the four successors).
However, in the presence of vector, matrix, and density matrix nodes, this is no longer as easy. There are many variations of triplets of the form (Operand1, Operand2, Result). It is unclear, how these could be combined in a feasible and efficient way. One possibility would be to resort to using variants for storing the individual types. But that would introduce a certain overhead in memory and runtime.

Describe the solution you'd like

Why bother?

  • has the potential to save memory overall by not having to maintain 10+ individual tables.
  • DD operations only ever scale with the number of nodes when the compute table is large enough to store all intermediate results required during a computation. By having multiple compute tables and reducing the size of some of them, the performance of some methods is inherently limited. Having one large table might actually allow to perform all operations in a performant way.
  • Having a single compute table makes managing its growth easier. While this is not an issue right now (compute tables are statically sized), the overall goal is to have them be dynamically resizable. Clearly, maintaining and orchestrating the growth of a single table depending on other package parameters is way easier than managing 10+ of them.
  • It might make the compute table itself more future proof and easier to extend for new operations and/or DD types.

As always, this has to be tested and evaluated well in order to guarantee that the resulting solution indeed fulfills its goals.

🐛 ZX PiExpression arithmetic not working properly

Environment information

Any version of the ZX library.

Description

The arithmetic for PiExpressions in the ZX package seems to be quite buggy in certain aspects.
For example

const auto beta = zx::PiRational(1, 2);

std::cout << beta * 2 << "\n";
std::cout << beta * 2. << "\n";

const auto betaExpr = zx::PiExpression{beta};
std::cout << betaExpr * 2. << "\n";
// std::cout << betaExpr * 2 << "\n"; // does not compile due to missing overload

yields

1/1
1/1
159154943/500000000

The last entry is completely unexpected.

So that's the one thing. This also means something like betaExpr / 2. will result in complete garbage.
Now, #482 adds support for divisions by integers (which allows to write the above as betaExpr / 2).
However, that revealed quite a problem in the way arithmetic expressions are handled.
Namely, that

const auto beta = zx::PiRational(-1, 2);
std::cout << (beta + beta) / 2 << "\n";  // and, if the above is fixed most likely also `(beta+beta)/2.` which currently returns garbage

prints

1/2

instead of the desired result of -1/2.
The reason for that is beta+beta evaluating to -1 which is 1 mod pi.

Expected behavior

I would expect division and multiplication by floating point numbers for PiExpression to work exactly as they do for PiRational.
Furthermore, I would expect moderately complex arithmetic expressions to produce the correct result.

How to Reproduce

Run the examples from above

🧪 Replace `pybind11` with `nanobind`

Nanobind (https://nanobind.readthedocs.io/en/latest/index.html) is a newer, faster and more lightweight alternative to pybind11.
The preferred way to install it (according to the docs) is directly via pip, which means fewer submodules for the project here.

There are a couple of things to take care of:

Tasks

Improvements for Trace Computation

At the moment, the partial_trace (and, hence, the trace) function in the package are calculated in a straight-forward fashion. As a result, the performance of the operation scales with the number of paths in the decision diagram as opposed to the number of nodes. Introducing a compute table as for the other decision diagram operations fixes this problem. A possible benchmark to test this is to simply create the matrix of an n-qubit Hadamard transform and compute its trace. This matrix has n nodes, but 4^n paths.

Furthermore, it might be possible to implement an improved trace function, that does not just call the partial trace and eliminates every variable, and instead just sums up all the diagonal entries.
It might even be beneficial to compute trace(...)/2^n in order to normalise the result to the range [0, 1]. Instead of computing trace(...) and then dividing by 2^n (which induces numerical problems), the computed trace can be divided by two in each of the levels in order to continuously normalise it throughout the computation.

Support for the inversion modifier of OpenQASM 3.0

The new OpenQASM standard introduces gate modifiers in order to more efficiently describe quantum circuits. Any modifier mod can be applied to a gate g via mod @ g. For more details, see Section 4.2 in https://arxiv.org/pdf/2104.14722.pdf or the Live Specification.

The inversion modifier inv can be used to invert any gate g via inv @ g. The inverse of any gate can be computed as:

  • For any unitary operation U = U_m-1 ... U_0 its inverse U^-1 is defined by reversing the order of operations and inverting each individual gate, i.e., U^-1 = U_0^-1 ... U_m-1^-1.
  • The inverse of a controlled operation is defined as the controlled inverse operation, i.e., inv @ ctrl @ g = ctrl @ inv @ g.
  • inv @ U(a, b, c) = U(-a, -c, -b) and inv @ gphase(a) = gphase(-a)

Note that the global phase gate gphase(a) is not yet introduced and its implementation is left for another issue at the moment.

We should be able to handle many of these inversions in a cleverer fashion, e.g., if it is known, that a gate is self-inverse, or e.g., in case of the phase gate, where the inverse is obtained by negating the parameter.

Enhancing the OpenQASM parser with this feature provides greater flexibility for describing circuits in a standardized way.

Tasks

🐍 Python bindings for the `QuantumComputation` class

At the moment, the QFR library does not provide any means to access or alter quantum circuits from the Python side.
For some of our endeavours it might be beneficial to, at least, get read-only access to the QuantumComputation class from Python.

This is closely tied with #35 since the Qiskit QuantumCircuit object would most certainly also provide all the options we wish for and we already have the means to translate Qiskit's QuantumCircuit class to our QuantumComputation class.

It would be neat to add corresponding methods in the python part of the codebase that allow to add corresponding bindings to any pybind11 module. That way, any top-level module can add these bindings to their Python modules without having to write everything from scratch.

Support for the control modifier of OpenQASM 3.0

The new OpenQASM standard introduces gate modifiers in order to more efficiently describe quantum circuits. Any modifier mod can be applied to a gate g via mod @ g. For more details, see Section 4.2 in https://arxiv.org/pdf/2104.14722.pdf or the Live Specification.

One of these is the ctrl modifier (and its sibling negctrl) that allows to add a control to any already defined gate. The CX gate, for example, is no longer a keyword in OpenQASM 3.0, but rather defined as:

gate cx c, t { ctrl @ x c, t; }

The negctrl modifier allows to describe controls with a negative polarity.

Both modifiers optionally accept an argument n which specifies the number of controls of the appropriate type, which are added to the front of the list of arguments. As a starter, n can be assumed to be an integer. This, e.g., allows to realize a Toffoli gate as

gate ccx c0, c1, t { ctrl(2) @ x c0, c1, t; }

The standard only requires n to be a "compile-time constant", which includes variables with a fixed value or iterator variables in for loops -- two concepts to be implemented separately.

Enhancing the OpenQASM parser with this feature should allow to simplify some of the complicated code that is used at the moment for handling controlled gates efficiently and provides greater flexibility for describing circuits in a standardized way.

Tasks

✨ Inverse gate for `iSwap`

What's the problem this feature will solve?

Currently, there is no inverse gate type for iSwap.

Describe the solution you'd like

Add a new gate type iSwapDag that represents the inverse of iSwap.

Compute Table Reset Improvements

At the moment, once garbage collection is called (and actually collects something), all compute tables are completely reset, i.e., emptied. While this is fast, it is not necessarily efficient. The compute table most probably contains many entries that are still valid (i.e., all constituents have a non-zero reference count).

The compute table reset should only clear the table from dead entries (i.e., where at least one of the edges has a zero reference count). In this fashion, valid entries are kept in the tables and can be used for further computations.

A prototypical implementation would be

  void clear() {
    if (count > 0) {
      for (auto& entry: table) {
        // If this is an unused entry, there is no need to clear it
        if (entry.leftOperand.p == nullptr && entry.rightOperand.p == nullptr) {
          continue;
        }

        // If all constituents of the entry have a non-zero reference count,
        // the entry is still valid and should not be cleared
        // This assumes that as long as a node is alive, the respective complex
        // numbers are alive as well.
        const auto leftAlive = entry.leftOperand.p == nullptr || entry.leftOperand.p->ref > 0;
        const auto rightAlive = entry.rightOperand.p == nullptr || entry.rightOperand.p->ref > 0;
        const auto resultAlive = entry.result.p == nullptr || entry.result.p->ref > 0;
        if (leftAlive && rightAlive && resultAlive) {
          continue;
        }

        entry = Entry{};
        --count;
      }
    }
  }

It's important to think about how often such a scenario might happen during multiplication and addition assuming the way we currently perform reference counting (e.g., the ref count of operation matrices is never increased).

✨ Potentially unify memory manager of number cache and table in DD package

What's the problem this feature will solve?

At the moment, there are two memory managers for real numbers: one that manages the cache and one that provides entries for the respective unique table. Since both effectively provide the same type of entry, the question arises whether they can be unified to a single memory manager for numbers.

Describe the solution you'd like

There are multiple sides to this:

  • the access patterns of the cache and the unique table are rather different. For the cache, many entries are taken from it during the course of an operation and are returned after the operation (putting them on the (linked) list of entries available for reuse. On the other hand, the whole purpose of the unique table is to persist numbers and only release them as part of garbage collection.
  • It seems wasteful to grow both memory managers, when one could simply use one. So this might reduce the overall memory footprint.
  • Since the list of entries available for reuse is always checked first when requesting a new entry, combining the memory managers might increase the average speed of allocating new nodes since the cache usage puts many entries there. Of course, new entries have to be taken from the allocated chunks by the cache usage if many nodes are used as unique table entries, but that trade off might as well be worth it.
  • There might actually be some potential for optimization in the lookup method: if a cached entry is looked up and is not already found in the unique table, then there would be no need to allocate a new entry and return the original one to the cache since the original entry could just be used and not returned. This might make it a little harder to check that the cache is maintained properly (there is no cache count before and cache count after any more). Most likely this can be resolved by keeping track of the number of entries in the unique table (which we should do anyway) and comparing that to the number of totally allocated numbers. Specifically, the cache count should be the number of total used numbers minus the total number of entries in the unique table.

Support for new OpenQASM 3.0 identifiers

The new OpenQASM standard introduces some new identifiers and relaxes some constraints on existing ones. In particular:

  • It is actually optional to specify the OPENQASM M.m statement as the first line of a QASM file.

  • OpenQASM 2.0 used qreg and creg for declaring quantum and classical register. Although these are still supported by the new 3.0 standard, the preferred identifiers are now qubit and bit.

  • Furthermore, names of registers and gates are no longer required to begin with a lower-case ASCII character. Identifiers may now begin with other characters, such as capital letters, underscores, and a range of unicode characters. For example, angular values used as gate arguments may now be represented by greek letters.

  • The identifier π is reserved in OpenQASM 3 to represent the same constant as pi

  • Additional constants included in the OpenQASM 3 standard are tau or τ for the value 2*pi and euler or for Euler's constant (2.7182818284...)

  • It also introduces a new syntax for expressing measurements. In addition to the existing syntax measure q[j] -> c[k], it also supports c[k] = measure q[j] for measurements.

  • Last but not least, barrier statements can appear without any target, which implies that a barrier should be applied targeting all qubits.

For more details, see https://arxiv.org/pdf/2104.14722.pdf or the Live Specification.

⚗️ Remove the `ToffoliTable` from the DD Package

The ToffoliTable data structure is used in the DD package to cache any decision diagrams for Toffoli gates that have been constructed.
This adds the need for special casing around Toffoli gates, introduces yet another lookup table that needs to be handled carefully, and hardly yields any performance benefits as the construction of Toffoli gates is linear in the number of qubits anyways.

Due to the above reason, the Toffoli Table should be removed in order to simplify the overall code.

In the future, it might be worth to investigate whether a general operation DD cache could be used to improve performance.

🐛 Minor issues due to global phase difference of `phase` and `rz` gate

The OpenQASM 2.0 specification lacks a notion of global phase tracking. The OpenQASM 2.0 standard library contains the following definitions:

gate u1(lambda) q { U(0,0,lambda) q; }
gate p(lambda) q { U(0,0,lambda) q; }
gate rz(phi) a { u1(phi) a; }

This means that, the phase gate p and the Z-rotation gate rz boil down to the same gate. However, these two differ by a global phase factor, which is why the OpenQASM 3.0 spec defines these gates as

gate u1(lambda) q { U(0, 0, lambda) q; }
gate p(lambda) a { ctrl @ gphase(lambda) a; }
gate rz(lambda) a { gphase(-lambda/2); U(0, 0, lambda) a; }

Now, a global phase is not a problem per-se and can be ignored. But once controls are added, this phase becomes relative and plays a role, i.e., a cp gate is different to a crz gate. However, in our QASM parser, we simply strip away leading c's, and look for a definition of the underlying gate. As a consequence, both gates (cp and crz) end up as cp in the resulting quantum circuit.

I believe the underlying problem can be fixed by refactoring our parser a little bit, so that it does not look up definitions of gates we natively support in the QFR. That approach is also fairly close to what Qiskit is doing and should allow to reduce the complexity of some parts of the parser actually. Most likely, this requires adding further tokens to the parser or, alternatively , introducing some kind of dictionary of supported gates that can be used for mapping identifiers to (Standard)Operations or OpTypes with controls, targets and parameters.

Note: This is only a problem for the QASM import. The Qiskit QuantumCircuit import properly handles the distinction between phase and RZ gates.

📝 Set up ReadTheDocs

The goal should be to setup a ReadTheDocs page similar to our top-level projects.
That page should at least cover all the installation and development steps.
Furthermore, it might take some inspiration from how @hillmich set up the C++ documentation in DDSIM a while back.
In the beginning, the C++ documentation will be pretty slim, as the code base is (unfortunately) rather badly documented.

Support for OpenQASM 3.0 looping and branching

The OpenQASM 3.0 standard provides the means to describe the flow of a program using if statements, for and while loops.
This allows to more easily write static quantum algorithms that consist of iterations (such as Grover's algorithm or Phase Estimation) but also allows for the construction of sequential quantum circuits (such as Repeat Until Success schemes).

As a starting point, the creation of static algorithms should be supported using these constructs, i.e., programs should be supported that only contain constants and hence make it possible to unroll all loops at compile-time. An example of such a circuit is shown in https://arxiv.org/pdf/2104.14722.pdf

/*
 * Iterative phase estimation
 */
OPENQASM 3;
include "stdgates.inc";

const n = 3; // number of iterations 
const θ = 3*π/8; //phase angle on target qubit

qubit q; // phase estimation qubit
qubit r; // target qubit for the controlled-unitary gate 
angle[n] c = 0; // phase estimation bits

// initialize
reset q;
reset r;

// prepare uniform superposition of eigenvectors of phase
h r;

// iterative phase estimation loop
for i in [1:n] { // implicitly cast val to int
  reset q;
  h q;
  ctrl @ pow(2**i) @ phase(θ) q, r;
  inv @ phase(c) q;
  h q;
  measure q -> c[0];
  // newest measurement outcome is associated to a π/2 phase shift
  // in the next iteration, so shift all bits of c left 
  c <<= 1;
}
// Now c contains the n-bit estimate of φ in the
// eigenvalue e^{i*φ} and qreg r is projected to an // approximate eigenstate of the phase gate.

In theory, this could be implemented independently from the other OpenQASM 3.0 features, but it probably makes sense to add support for constant values first (#31).

:recycle: Refactor handling of gate matrices and inverses

After #460, all gates in MQT Core have a proper inverse now.
A natural follow-up would be to consolidate the following methods:

  • void StandardOperation::invert() {
    switch (type) {
    // self-inverting gates
    case I:
    case X:
    case Y:
    case Z:
    case H:
    case SWAP:
    case ECR:
    case Barrier:
    break;
    // gates where we just update parameters
    case GPhase:
    case P:
    case RX:
    case RY:
    case RZ:
    case RXX:
    case RYY:
    case RZZ:
    case RZX:
    parameter[0] = -parameter[0];
    break;
    case U2:
    std::swap(parameter[0], parameter[1]);
    parameter[0] = -parameter[0] + PI;
    parameter[1] = -parameter[1] - PI;
    break;
    case U:
    parameter[0] = -parameter[0];
    parameter[1] = -parameter[1];
    parameter[2] = -parameter[2];
    std::swap(parameter[1], parameter[2]);
    break;
    case XXminusYY:
    case XXplusYY:
    parameter[0] = -parameter[0];
    break;
    case DCX:
    std::swap(targets[0], targets[1]);
    break;
    // gates where we have specialized inverted operation types
    case S:
    type = Sdg;
    break;
    case Sdg:
    type = S;
    break;
    case T:
    type = Tdg;
    break;
    case Tdg:
    type = T;
    break;
    case V:
    type = Vdg;
    break;
    case Vdg:
    type = V;
    break;
    case SX:
    type = SXdg;
    break;
    case SXdg:
    type = SX;
    break;
    case Peres:
    type = Peresdg;
    break;
    case Peresdg:
    type = Peres;
    break;
    // Tracking issue for iSwap: https://github.com/cda-tum/mqt-core/issues/423
    case iSWAP:
    case None:
    case Compound:
    case Measure:
    case Reset:
    case Teleportation:
    case ClassicControlled:
    case ATrue:
    case AFalse:
    case MultiATrue:
    case MultiAFalse:
    case OpCount:
    throw QFRException("Inverting gate" + toString(type) +
    " is not supported.");
    }
  • switch (type) {
    case qc::I:
    gm = I_MAT;
    break;
    case qc::H:
    gm = H_MAT;
    break;
    case qc::X:
    gm = X_MAT;
    break;
    case qc::Y:
    gm = Y_MAT;
    break;
    case qc::Z:
    gm = Z_MAT;
    break;
    case qc::S:
    gm = inverse ? SDG_MAT : S_MAT;
    break;
    case qc::Sdg:
    gm = inverse ? S_MAT : SDG_MAT;
    break;
    case qc::T:
    gm = inverse ? TDG_MAT : T_MAT;
    break;
    case qc::Tdg:
    gm = inverse ? T_MAT : TDG_MAT;
    break;
    case qc::V:
    gm = inverse ? VDG_MAT : V_MAT;
    break;
    case qc::Vdg:
    gm = inverse ? V_MAT : VDG_MAT;
    break;
    case qc::U:
    gm = inverse ? uMat(-parameter[1U], -parameter[2U], -parameter[0U])
    : uMat(parameter[2U], parameter[1U], parameter[0U]);
    break;
    case qc::U2:
    gm = inverse ? u2Mat(-parameter[0U] + PI, -parameter[1U] - PI)
    : u2Mat(parameter[1U], parameter[0U]);
    break;
    case qc::P:
    gm = inverse ? pMat(-parameter[0U]) : pMat(parameter[0U]);
    break;
    case qc::SX:
    gm = inverse ? SXDG_MAT : SX_MAT;
    break;
    case qc::SXdg:
    gm = inverse ? SX_MAT : SXDG_MAT;
    break;
    case qc::RX:
    gm = inverse ? rxMat(-parameter[0U]) : rxMat(parameter[0U]);
    break;
    case qc::RY:
    gm = inverse ? ryMat(-parameter[0U]) : ryMat(parameter[0U]);
    break;
    case qc::RZ:
    gm = inverse ? rzMat(-parameter[0U]) : rzMat(parameter[0U]);
    break;
    default:
    std::ostringstream oss{};
    oss << "DD for gate" << op->getName() << " not available!";
    throw qc::QFRException(oss.str());
    }
    return dd->makeGateDD(gm, nqubits, controls, target, startQubit);
    }
  • switch (type) {
    case qc::SWAP:
    gm = SWAP_MAT;
    break;
    case qc::iSWAP:
    gm = inverse ? ISWAPDG_MAT : ISWAP_MAT;
    break;
    case qc::DCX:
    gm = DCX_MAT;
    break;
    case qc::ECR:
    gm = ECR_MAT;
    break;
    case qc::RXX:
    gm = inverse ? rxxMat(-parameter[0U]) : rxxMat(parameter[0U]);
    break;
    case qc::RYY:
    gm = inverse ? ryyMat(-parameter[0U]) : ryyMat(parameter[0U]);
    break;
    case qc::RZZ:
    gm = inverse ? rzzMat(-parameter[0U]) : rzzMat(parameter[0U]);
    break;
    case qc::RZX:
    gm = inverse ? rzxMat(-parameter[0U]) : rzxMat(parameter[0U]);
    break;
    case qc::XXminusYY:
    gm = inverse ? xxMinusYYMat(-parameter[0U], parameter[1U])
    : xxMinusYYMat(parameter[0U], parameter[1U]);
    break;
    case qc::XXplusYY:
    gm = inverse ? xxPlusYYMat(-parameter[0U], parameter[1U])
    : xxPlusYYMat(parameter[0U], parameter[1U]);
    break;
    default:
    definitionFound = false;
    }
    if (definitionFound) {
    return dd->makeTwoQubitGateDD(gm, nqubits, target0, target1, startQubit);
    }
    }
    switch (type) {
    case qc::SWAP:
    // SWAP is self-inverse
    return dd->makeSWAPDD(nqubits, controls, target0, target1, startQubit);
    case qc::iSWAP:
    if (inverse) {
    return dd->makeiSWAPinvDD(nqubits, controls, target0, target1,
    startQubit);
    }
    return dd->makeiSWAPDD(nqubits, controls, target0, target1, startQubit);
    case qc::Peres:
    if (inverse) {
    return dd->makePeresdagDD(nqubits, controls, target0, target1,
    startQubit);
    }
    return dd->makePeresDD(nqubits, controls, target0, target1, startQubit);
    case qc::Peresdg:
    if (inverse) {
    return dd->makePeresDD(nqubits, controls, target0, target1, startQubit);
    }
    return dd->makePeresdagDD(nqubits, controls, target0, target1, startQubit);
    case qc::DCX:
    return dd->makeDCXDD(nqubits, controls, target0, target1, startQubit);
    case qc::ECR:
    // ECR is self-inverse
    return dd->makeECRDD(nqubits, controls, target0, target1, startQubit);
    case qc::RXX: {
    if (inverse) {
    return dd->makeRXXDD(nqubits, controls, target0, target1, -parameter[0U],
    startQubit);
    }
    return dd->makeRXXDD(nqubits, controls, target0, target1, parameter[0U],
    startQubit);
    }
    case qc::RYY: {
    if (inverse) {
    return dd->makeRYYDD(nqubits, controls, target0, target1, -parameter[0U],
    startQubit);
    }
    return dd->makeRYYDD(nqubits, controls, target0, target1, parameter[0U],
    startQubit);
    }
    case qc::RZZ: {
    if (inverse) {
    return dd->makeRZZDD(nqubits, controls, target0, target1, -parameter[0U],
    startQubit);
    }
    return dd->makeRZZDD(nqubits, controls, target0, target1, parameter[0U],
    startQubit);
    }
    case qc::RZX: {
    if (inverse) {
    return dd->makeRZXDD(nqubits, controls, target0, target1, -parameter[0U],
    startQubit);
    }
    return dd->makeRZXDD(nqubits, controls, target0, target1, parameter[0U],
    startQubit);
    }
    case qc::XXminusYY: {
    if (inverse) {
    return dd->makeXXMinusYYDD(nqubits, controls, target0, target1,
    -parameter[0U], parameter[1U], startQubit);
    }
    return dd->makeXXMinusYYDD(nqubits, controls, target0, target1,
    parameter[0U], parameter[1U], startQubit);
    }
    case qc::XXplusYY: {
    if (inverse) {
    return dd->makeXXPlusYYDD(nqubits, controls, target0, target1,
    -parameter[0U], parameter[1U], startQubit);
    }
    return dd->makeXXPlusYYDD(nqubits, controls, target0, target1,
    parameter[0U], parameter[1U], startQubit);
    }
    default:
    std::ostringstream oss{};
    oss << "DD for gate" << op->getName() << " not available!";
    throw qc::QFRException(oss.str());
    }
    }

There is lots of redundancy in the handling of the inverse throughout the code base.
I do not have a perfect solution for resolving that in mind. It would be pretty convenient though if any operation could return its gate matrix. So essentially to refactor https://github.com/cda-tum/mqt-core/blob/main/include/dd/GateMatrixDefinitions.hpp so that the appropriate matrix can be queried from an Operation.
To this end, it could be nice to adopt a strategy similar to Qiskit where there are individual classes for gates that all derive from StandardOperation (and potentially SymbolicOperation for parametrized gates). As an example:

  • there would be an XGate class that provides a method to get the single-qubit GateMatrix, the type of the gate (already covered by StandardOperation), the inverse type, the inverse GateMatrix and potentially other useful methods.
  • The challenge will be to get the interface right so that there is a standardized way to call the newly introduced methods. In that regard, further compile-time/constexpr convenience functions like isSingleTargetGate, isTwoTargetGate, and isThreeOrMoreTargetGate would probably make sense.
  • Naturally, this creates quite some new classes, but I believe it makes the library structure a little simpler. Needs a little experimenting on the refactor though.
  • Maybe the above actually makes some of the operations (such as iSWAPdg redundant again.

Originally posted by @burgholzer in #460 (review)

🐛 Multiple dots in file path passed to QuantumComputation::dump results in assertion fail

Environment information

  • mqt.core version: commit #433

Description

When passing a file name containing more than 1 dot (e.g. test.circuit.qasm) to QuantumComputation::dump (or by extension any of its wrapper functions like QuantumComputation::dumpOpenQASM), the assertion in line 716 of QuantumComputation.cpp fails:

assert(std::count(filename.begin(), filename.end(), '.') == 1);

If I understand this line correctly, it tries to assert, that the supplied file name includes a file extension (though I'm not quite sure, why this would even be necessary in line 716, where the dumping format is already clear; maybe this line actually belongs before current line 622 const std::size_t dot = filename.find_last_of('.');?).
In any case, if the only objective is to test for the existence of a file extension, shouldn't it be:

assert(std::count(filename.begin(), filename.end(), '.') >= 1);

Expected behavior

I would expect to be able to pass any valid file name (or even file path) to QuantumComputation::dump containing a valid quantum circuit file extension but no other constraints (and in case I supply an explicit qc::Format even the file extension to be irrelevant)

How to Reproduce

qc::QuantumComputation qc{4, 4};
qc.x(0, qc::Control{1});
qc.x(2, qc::Control{3});
qc.dump("test.circuit.qasm", qc::Format::OpenQASM);
// or
qc.dump("./test/circuit.qasm", qc::Format::OpenQASM); // second point not even in file name itself but only in parent path

⚗️ Try to get rid of the `start` parameter in DD routines

Many of the DD routines throughout the package include a start parameter that is supposed to indicate the lowest index in the DD. This was mainly introduced for the Hybrid Schrödinger-Feynman (HSF) simulator and isn't used anywhere else.
As such, it is kind of a nuisance and leads to a lot of special handling throughout some of the functions.

It should be possible to work around this requirement by refactoring parts of the HSF simulator in conjunction with the package code here.

Related upstream-stream issue:

⚡ Better performance in Qiskit import

What's the problem this feature will solve?

Right now, the Qiskit import introduced in #371 incurs some unnecessary copies when translating operations from Qiskit to our internal representation.
This is because the code is organised so that it can simultaneously/recursively handle CompoundOperations and QuantumComputations. However, only the QuantumComputation class has convenience methods for directly constructing gates in-place right now.
As a consequence, the Qiskit import just always constructs an operation and then moves it to the QuantumComputation or CompoundOperaton (incurring a copy).

Describe the solution you'd like

It should be possible to avoid the copy and always construct the operations in-place (even if they are nested in a CompoundOperation).
Most likely, the solution is to replace the current call sites where a Compound operation is used by creating a QuantumComputation, then recursively calling the gate import on that circuit (using the convenience functions for adding gates in-place), and afterwards use the to_operation method to turn the circuit into a CompoundOperation that can then be added to the circuit.

An alternative solution involves also exposing convenience functions for adding gates in-place to the CompoundOperation class similar to the QuantumComputation. However, this creates quite some overhead in documentation and in terms of lines of code and is, therefore, not the preferred solution.

Make max. qubits independent from DD package

At the moment, the maximum number of qubits for quantum circuits is limited by the maximum number allowed by the underlying decision diagram package (128 per default). This is an unnecessary restriction and can certainly be loosened.
This should allow for more scalable usage of the QFR library when it is not used in conjunction with decision diagrams, e.g., for error correcting codes, compilation, stabilizer-based simulation of Clifford circuits.

✨ Adjust optimization passes for parameterized circuits

At the moment, circuit optimization passes fail when dealing with QuantumComputations involving SymbolicOperations. Many (if not all) of the optimization passes do not require the circuit to consist entirely of non-symbolic operations (like SWAP reconstruction).

Optimization passes should be applicable just like with non-parameterized circuits. This requires minor adjustments in the respective optimization passes. Some optimization passes (like single-qubit gate fusion) might even incorporate symbolic operations into the optimization.

♻️ Refactor functions to avoid the generateDensityMatrix flag

I am not a fan of adding the generateDensityMatrix flag to a ton of functions.

Unfortunately, C++ does not allow two functions that only differ in their return values. Otherwise it would be rather easy to get rid of this generateDensityMatrix parameter, which is only relevant for density matrices.

One potential solution for this would be to refactor these methods in the following fashion:

template<class LeftOperandNode, class RightOperandNode, class ResultNode>
void multiply2(const const Edge<LeftOperandNode>& x, const Edge<RightOperandNode>& y, Edge<ResultNode>& result, Qubit var, Qubit start = 0) {
  ...
}

This would allow for specializations of Matrix * Matrix -> Matrix and Matrix * Matrix -> Density Matrix.

Originally posted by @burgholzer in cda-tum/dd_package#72 (comment)

ecc::Q18Surface code: phase flip correction

    How temporary is this deactivation? Something to be fixed before this PR is merged?

If not, then please create an issue for that. Otherwise I am fairly certain this will get lost.

Originally posted by @burgholzer in #210 (comment)

Currently, the ecc::Q18Surface error correction code only corrects bit flips.
The basic procedure for correcting phase errors is very similar to bit flip correction, however, only the initialization of ancilla qubits takes more effort and thus still has to be done.

Strategy: Starting with the 0-state, we perform a "measure-and-correct" step. To work correctly, this step has to detect and correct multiple errors at once. In the current implementation (already used for bit flip correction), multiple errors can be detected, but only single errors can be corrected.

Support for the powering modifier of OpenQASM 3.0

The new OpenQASM standard introduces gate modifiers in order to more efficiently describe quantum circuits. Any modifier mod can be applied to a gate g via mod @ g. For more details, see Section 4.2 in https://arxiv.org/pdf/2104.14722.pdf or the Live Specification.

The powering modifier pow(r) can be used to represent the r-th power of any gate g via pow(r) @ g, where r can either be an integer or a floating point number.

The case where r is a floating point number needs to be handled by computing the principal logarithm of the unitary that represents g. This might be tricky in general.

The case where r is an integer can be naively solved by repeatedly applying g. If r <0, pow(r) @ g can be translated to inv @ pow(-r) @ g. Several simplification might be possible here, e.g., the powers of phase gates can easily be computed by appropriately scaling the respective parameter. In its simplest form pow(2) @ s = z.

At the very least, support for the case where r is an integer shall be provided which provides much greater flexibility for describing circuits in a standardized way.

Tasks

⚡ Natively construct two-target controlled-gate DDs

In cda-tum/dd_package#144, a function was added to the DD package that allows the native construction of DDs for two-target gates. This is a significant improvement compared to the old-fashioned way of constructing these DDs from the DDs of a gate decomposition for the respective gate (e.g., a SWAP DD can be constructed from 2 CNOT DDs).

However, the implementation in cda-tum/dd_package#144 only works for non-controlled two-target gates. Thus, controlled versions of these gates still require the old-fashioned workaround. This can be seen in https://github.com/cda-tum/dd_package/blob/7c9644a2a65742bb580e3e5fefeac075c9534351/include/dd/Package.hpp#L629-L810
All of that code could be avoided by making the construction routine handle (arbitrary) controls as well. The single-target method already does this as shown in https://github.com/cda-tum/dd_package/blob/7c9644a2a65742bb580e3e5fefeac075c9534351/include/dd/Package.hpp#L480-L539

This requires some careful handling of all possible cases (control before first target, control between both targets, control after second target) and respective testing.

I believe this is a nice little project as it is very localized and would eliminate quite some code here and in our top-level modules.
A best case scenario would be to find a generic solution for multi-target gates, so that the makeGateDD and makeTwoQubitGateDD methods can be unified. Hypothetically that further abstraction shouldn't be too hard once one has figured out how to handle the "control between targets" case.

✨ State Preparation Routines for DD Package

Currently, the DD package provides methods for directly constructing the decision diagrams for

  • computational basis states
  • any product state that can be constructed from the single-qubit states |0>, |1>, |+>, |->, |L>, |R>

It would be convenient to have some further state preparations available that can be used, e.g., in DDSIM.
Examples:

  • GHZ state
  • W state
  • ... some other important states ...

It should be possible to directly construct the DDs for these states without starting from the all-zero state and applying some operations.

A suggestion would be to name these functions make<SOME-IDENTIFIER>State.
In the interest of providing a little more structure in the DD package, these functions should probably be designed in a way that they can be put into a separate header file and, ideally, not rely on any template stuff, so that the implementation can be moved to a source file.
If this is accomplished, the existing functions for state preparation should be moved as well.

Support for casting of OpenQASM 3.0 types

The new OpenQASM standard introduces many additional datatypes besides quantum and classical registers. This allows to describe classical components of quantum algorithms in a very intuitive way. As a first step, support for all kinds of new types should be added to the QFR library and, correspondingly, to the QuantumComputation class. For details on all the new typed provided by the standard, see https://qiskit.github.io/openqasm/language/classical.html.

The standard provides the means for converting between the available types. For details, see https://qiskit.github.io/openqasm/language/types.html#casting-specifics.

This depends on #30 for the availability of all the types.

✨ An option to disable ASLR

What's the problem this feature will solve?

The DD package as it is currently implemented relies on the memory addresses of nodes for hashing (pointer-based DD package). Modern compilers and operating systems implement Address Space Layout Randomization (ASLR) as a security measure to randomize the location of code.
This, however, leads to non-deterministic behavior of the decision diagram package because different invocations of the same computation are allocated in different memory regions and hence hash values might differ.

Describe the solution you'd like

It would be convenient, especially for debugging, to have an option for disabling ASLR and, thus, enabling reproducible builds.
AFAIK, the way to achieve this is platform-specific, but there should be enough about this out there.
Ideally, it gets integrated into the cmake/ folder as an additional standalone script.

Note
This issue becomes somewhat obsolete if we ever decide to switch to an index-based DD package implementation.

✨ QIR support for MQT

What's the problem this feature will solve?

This is a tracking issue for adding QIR support within the MQT.
The following contains a summary of some discussions that took place at QCE23.

Describe the solution you'd like

One of the key take aways is that it is fine to keep our own circuit representation, i.e., we are not expected to base all of our developments on an LLVM Parse-Tree and throw away the QuantumComputation class.
In principle, we need to be able to parse QIR into QuantumComputation objects and create QIR from a QuantumComputation description.

The ad-hoc, straight-forward solution would be to rely on the PyQIR Python library that facilitates easy parsing and writing of QIR.
As for translating our circuit structure to QIR, https://github.com/microsoft/qiskit-qir could serve as an inspiration.
Pros:

  • easy to adopt
  • similar interface to the existing Qiskit support within the MQT
  • no heavy dependency (QIR support would just be an extra of the Python package)
    Cons:
  • Python (slow, not HPC compatible)
  • Does not really make sense long-term to parse to Python via a Python Package that is build on top of a rust package that wraps LLVM (which is purely C++)

A potential alternative would be to use the qirlib Rust library that builds the foundation for PyQIR and interface with that.
Pros:

  • Close-to-native Performance
  • Would bring Rust into the MQT Ecosystem
    Cons:
  • Requires us to adopt Rust in the MQT ecosystem (Compiler, CI, etc.)
  • Requires us to interface with a Rust library from C++ that itself binds to the underlying LLVM C++ code

As a consequence of the above, it was actually recommended to directly build upon LLVM, which is natively written in C++.
Pros:

  • Native solution
  • Most performance
  • Truly in-house solution
    Cons:
  • Code duplication between the qirlib and our in-house solution
  • Requires the adoption of parts of the LLVM code base into our toolkit
  • Steep entry barrier and learning curve

As for the last part, I was told that the part of LLVM that’s actually needed for QIR support should be rather small compared to the complete LLVM Suite.

From these discussion, I would rule out the second option from above as it sits straight in between the least effort/quickest solution and the most effort/long-term solution.
I would prefer the long-term solution to be implemented right away, although it might be a tough nut to crack.

A couple of references for starting out with this:

Tasks

✨ Eliminate global variables from DD package

What's the problem this feature will solve?

The DD package contains a “fine collection” of (non-const) global variables. This includes the terminal nodes for DDs as well as the real number constants for 0., 1., 1./sqrt(2) and the complex number constants zero and one. All of these are defined as (non-const) static variables with global scope. This has a number of drawbacks:

  • it is a bad coding practice to have non-const global variables (clang-tidy even warns about it).
  • It leads to all kinds of problems with static initialization and the order of initialization (clang-tidy even warns about that)
  • It was one of the main reasons why the MQT had problems building under Windows with MSVC. And took quite some while to get working.
  • These “constant” entries are never really constant; they are mutable. Not only does this convey the wrong intentions, but it also increases the potential for introducing errors when these entries are erroneously modified.
  • Due to these special entities actually being static instances of the respective class, their memory address differs across runs and, in the worst case (without proper care), between translation units.
  • Due to these special entities being regular class instances, it is mostly assumed that any pointer member may just be dereferenced (as nullptr should never really occur). While this might seem beneficial at first, it promotes not using the member functions of the respective classes that safeguard the access and rather directly manipulating the dereferenced instances. Especially since we make use of unaligned pointers, this can be quite dangerous.
  • Minor: all the data members of these special entities (such as ref count, next pointer, etc.) are never actually used. So it is kind of a waste to allocate them.

Describe the solution you'd like

This list of undesirable behavior is arguably quite long. So what could be done about it?
Since we are heavily dealing with pointer types, there is one rather straight forward solution: replace these global (non-)constants with clever uses of nullptr.

For DD terminal nodes, that is fairly straightforward. Up until now, a terminal node was a pointer to a global static Node that had variable index -1. The successors of that node were never accessed and any check for a terminal either compared addresses with the pointer to the terminal node or checked the variable index. This kind of usage of the variable index necessitated a signed type for that variable and, hence, halved the maximal number of qubits that could be supported by the package from 255 to 128.
The notion of a terminal can simply be replaced by the nullptr without any apparent downsides.

  • Checking whether a node is a terminal remains a memory address comparison. It now, would just be a simple nullptr check. This should really be a one to one replacement.
  • After that change, the data type of the variable index might be switched to an unsigned one, so that more qubits become available as part of the package. This might require some more changes in the code as I would suspect that there are quite some checks in the code base that have the form ˋif(p->v == -1)ˋ. These should be adjusted accordingly to benefit from the enhanced qubit range.

For the real number constants, the story is not so easy. The main problem here is that there is not a single, but three different constants that are being used (0., 1., and 1/sqrt(2)). Thus, these constants can’t simply all be replaced by using nullptr. Furthermore, the value of these entries are actually used in computations. However, there is a potential solution for that:
We already make use of the LSB of pointers to numbers to indicate that they are negative. Why not turn that up a notch and make use of the available scratch space in the lower bits of the pointers? (Due to alignment reasons, the lower three bits of any valid pointer are zero, which leaves 3 bits for hiding some stuff in pointers). The following hinges on one assumption: nullptr lives at address 0x0. Instead of creating global (non-)constants, the nullptr and its least significant three bits can be used to encode these special values:

  • As before, the least significant bit encodes the sign of the number.
  • 0x0 encodes 0.
  • 0x2 encodes 1.
  • 0x4 encodes 1/sqrt(2)
  • This leaves room for one more number to encode. Here, it might make a lot of sense to use 0.5 (which is then encoded as 0x6). The number 0.5 is currently unconditionally added to the real number table and never collected. Might as well make it a constant.
  • Checking for a particular constant remains as easy as before and is a simple pointer comparison.
  • Checking wether a given pointer points to any constant is as simple as masking out the three least significant bits and comparing to 0x0.
  • Getting the value for a constant number can be efficiently handled by a size 8 lookup table that is based on the three least significant bits of a pointer.
  • The big question is: how high is the overhead of having to add an additional if condition in the access function to the value of a number (for checking whether the number is a constant). And, can this overhead sometimes be avoided if it is clear that none of the entries is a constant? This has to be benchmarked an investigated.

The good thing is: these two changes are independent from one another and can happen in parallel (or the complex number change might not happen at all).

Tasks

Export to Qiskit `QuantumCircuit`

The QFR already provides the means to import a Qiskit QuantumCircuit into our own C++ qc::QuantumComputation (see https://github.com/iic-jku/qfr/blob/master/jkq/qfr/qiskit/QuantumCircuit.hpp).

In the future, it might be beneficial to provide support for translating a C++ qc::QuantumComputation to a Qiskit QuantumCircuit. At the moment, this is handled via translating to QASM and then loading the resulting string into Qiskit.
Possible applications could be the compilation of quantum circuits.

Important aspects to think about are how the initial_layout and the output_permutation are translated to Qiskit.
As of (I believe) qiskit-terra>=0.23, circuits have a _layout property that can store the initial_layout as well as the final_layout of the circuit.
In addition, the final layout should be indicated via measurements at the end of the circuits.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.