Skip to content

Commit

Permalink
Added main utility modules
Browse files Browse the repository at this point in the history
* state preparation
* logging
* register
* execution helpers
* qubit support
* serialization
* typing
* wavefunction overlaps

Co-authored-by: Aleksander Wennersteen <[email protected]>
Co-authored-by: Mario Dagrada <[email protected]>
Co-authored-by: Vincent Elfving <[email protected]>
Co-authored-by: Dominik Seitz <[email protected]>
Co-authored-by: Joao Moutinho <[email protected]>
Co-authored-by: Vytautas Abramavicius <[email protected]>
Co-authored-by: Niklas Heim <[email protected]>
Co-authored-by: Roland Guichard <[email protected]>
  • Loading branch information
8 people committed Oct 2, 2023
1 parent d73b9a1 commit 6d785c7
Show file tree
Hide file tree
Showing 14 changed files with 2,908 additions and 0 deletions.
65 changes: 65 additions & 0 deletions qadence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

from importlib import import_module

from torch import cdouble, set_default_dtype
from torch import float64 as torchfloat64

from .backend import *
from .backends import *
from .blocks import *
from .circuit import *
from .constructors import *
from .errors import *
from .execution import *
from .measurements import *
from .ml_tools import *
from .models import *
from .operations import *
from .overlap import *
from .parameters import *
from .register import *
from .serialization import *
from .states import *
from .transpile import *
from .types import *
from .utils import *

DEFAULT_FLOAT_DTYPE = torchfloat64
DEFAULT_COMPLEX_DTYPE = cdouble
set_default_dtype(DEFAULT_FLOAT_DTYPE)
"""
The imports above fetch the functions defined in the __all__ of each sub-module
to the qadence name space. Make sure each added submodule has the respective definition:
- `__all__ = ["function0", "function1", ...]`
Furthermore, add the submodule to the list below to automatically build
the __all__ of the qadence namespace. Make sure to keep alphabetical ordering.
"""

list_of_submodules = [
".backends",
".blocks",
".circuit",
".constructors",
".errors",
".execution",
".measurements",
".ml_tools",
".models",
".operations",
".overlap",
".parameters",
".register",
".serialization",
".states",
".transpile",
".types",
".utils",
]

__all__ = []
for submodule in list_of_submodules:
__all_submodule__ = getattr(import_module(submodule, package="qadence"), "__all__")
__all__ += __all_submodule__
204 changes: 204 additions & 0 deletions qadence/circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from __future__ import annotations

from dataclasses import dataclass
from itertools import chain as flatten
from pathlib import Path
from typing import Iterable

from sympy import Array, Basic

from qadence.blocks import AbstractBlock, AnalogBlock, CompositeBlock, chain
from qadence.blocks.utils import parameters, primitive_blocks
from qadence.parameters import Parameter
from qadence.register import Register

# Modules to be automatically added to the qadence namespace
__all__ = ["QuantumCircuit"]


@dataclass(eq=False) # Avoid unhashability errors due to mutable attributes.
class QuantumCircuit:
"""A QuantumCircuit instance is completely abstract and it needs to be passed to a quantum
backend in order to be executed.
"""

block: AbstractBlock
register: Register

def __init__(self, support: int | Register, *blocks: AbstractBlock):
"""
Arguments:
support: `Register` or number of qubits. If an integer is provided, a register is
constructed with `Register.all_to_all(x)`
*blocks: (Possibly multiple) blocks to construct the circuit from.
"""
self.block = chain(*blocks) if len(blocks) != 1 else blocks[0]
self.register = Register(support) if isinstance(support, int) else support

global_block = isinstance(self.block, AnalogBlock) and self.block.qubit_support.is_global
if not global_block and len(self.block) and self.block.n_qubits > self.register.n_qubits:
raise ValueError(
f"Register with {self.register.n_qubits} qubits is too small for the "
f"given block with {self.block.n_qubits} qubits"
)

@property
def n_qubits(self) -> int:
return self.register.n_qubits

def __eq__(self, other: object) -> bool:
if not isinstance(other, QuantumCircuit):
raise TypeError(f"Cannot compare {type(self)} to {type(other)}.")
if self.block != other.block: # type: ignore[call-overload]
return False
if self.register != other.register:
return False
return True

def __hash__(self) -> int:
return hash(self._to_json())

def __iter__(self) -> Iterable:
if isinstance(self.block, CompositeBlock):
yield from self.block
else:
yield self.block

def __contains__(self, other: object) -> bool:
if isinstance(other, AbstractBlock):
if isinstance(self.block, CompositeBlock):
return other in self.block
else:
return other == self.block
elif isinstance(other, Parameter):
return other in self.unique_parameters
else:
raise TypeError(f"Cant compare {type(self)} to {type(other)}")

@property
def unique_parameters(self) -> list[Parameter]:
"""Return the unique parameters in the circuit
These parameters are the actual user-facing parameters which
can be assigned by the user. Multiple gates can contain the
same unique parameter
Returns:
list[Parameter]: List of unique parameters in the circuit
"""
symbols = []
for p in parameters(self.block):
if isinstance(p, Array):
continue
elif not p.is_number and p not in symbols:
symbols.append(p)
return symbols

@property
def num_unique_parameters(self) -> int:
return len(self.unique_parameters) if self.unique_parameters else 0

@property
def num_parameters(self) -> int:
return len(self.parameters())

def parameters(self) -> list[Parameter | Basic] | list[tuple[Parameter | Basic, ...]]:
"""Extract all parameters for primitive blocks in the circuit
Notice that this function returns all the unique Parameters used
in the quantum circuit. These can correspond to constants too.
Returns:
List[tuple[Parameter]]: A list of tuples containing the Parameter
instance of each of the primitive blocks in the circuit or, if the `flatten`
flag is set to True, a flattened list of all circuit parameters
"""
return parameters(self.block)

def get_blocks_by_tag(self, tag: str) -> list[AbstractBlock]:
"""Extract one or more blocks using the human-readable tag
This function recurservily explores all composite blocks to find
all the occurrences of a certain tag in the blocks
Args:
tag (str): the tag to look for
Returns:
list[AbstractBlock]: The block(s) corresponding to the given tag
"""

def _get_block(block: AbstractBlock) -> list[AbstractBlock]:
blocks = []
if block.tag == tag:
blocks += [block]
if isinstance(block, CompositeBlock):
blocks += flatten(*[_get_block(b) for b in block.blocks])
return blocks

return _get_block(self.block)

def is_empty(self) -> bool:
return len(primitive_blocks(self.block)) == 0

def serialize(self) -> str:
raise NotImplementedError

@staticmethod
def deserialize(json: str) -> QuantumCircuit:
raise NotImplementedError

def __repr__(self) -> str:
return self.block.__repr__()

def _to_dict(self) -> dict:
return {
"block": self.block._to_dict(),
"register": self.register._to_dict(),
}

def _to_json(self, path: Path | str | None = None) -> str:
import json

qc_dumped = json.dumps(self._to_dict())
if path is not None:
path = Path(path)
try:
with open(path, "w") as file:
file.write(qc_dumped)
except Exception as e:
print(f"Unable to write QuantumCircuit to disk due to {e}")

return qc_dumped

@classmethod
def _from_dict(cls, d: dict) -> QuantumCircuit:
from qadence import blocks as qadenceblocks
from qadence import operations

RootBlock = (
getattr(operations, d["block"]["type"])
if hasattr(operations, d["block"]["type"])
else getattr(qadenceblocks, d["block"]["type"])
)

return QuantumCircuit(
Register._from_dict(d["register"]),
RootBlock._from_dict(d["block"]),
)

@classmethod
def _from_json(cls, path: str | Path) -> QuantumCircuit:
import json

loaded_dict: dict = {}
if isinstance(path, str):
path = Path(path)
try:
with open(path, "r") as file:
loaded_dict = json.load(file)

except Exception as e:
print(f"Unable to load QuantumCircuit due to {e}")

return QuantumCircuit._from_dict(loaded_dict)
37 changes: 37 additions & 0 deletions qadence/divergences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

from collections import Counter

import numpy as np


def shannon_entropy(counter: Counter) -> float:
return float(-np.sum([count * np.log(count) for count in counter.values()]))


def js_divergence(counter_p: Counter, counter_q: Counter) -> float:
"""
Compute the Jensen-Shannon divergence between two probability distributions
represented as Counter objects.
The JSD is calculated using only the shared keys between the two input Counter objects.
Args:
counter_p (Counter): Counter of bitstring counts for probability mass function P.
counter_q (Counter): Counter of bitstring counts for probability mass function Q.
Returns:
float: The Jensen-Shannon divergence between counter_p and counter_q.
"""
# Normalise counters
normalisation_p = np.sum([count for count in counter_p.values()])
normalisation_q = np.sum([count for count in counter_q.values()])
counter_p = Counter({k: v / normalisation_p for k, v in counter_p.items()})
counter_q = Counter({k: v / normalisation_q for k, v in counter_q.items()})

average_proba_counter = counter_p + counter_q
average_proba_counter = Counter({k: v / 2.0 for k, v in average_proba_counter.items()})
average_entropy = shannon_entropy(average_proba_counter)

entropy_p = shannon_entropy(counter_p)
entropy_q = shannon_entropy(counter_q)
return float(average_entropy - (entropy_p + entropy_q) / 2.0)
Loading

0 comments on commit 6d785c7

Please sign in to comment.