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

Quantum circuit and state models for IR integration #21

Merged
merged 101 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
e26a180
add ir
thisac May 20, 2021
ba4be4d
Merge branch 'main' into add-ir
thisac May 20, 2021
5c82fc5
move tests
thisac May 20, 2021
20ff84c
run black
thisac May 20, 2021
a250727
Rename ir/ directories to xir/
Mandrenkov May 21, 2021
c6da8b8
Apply formatter and update import paths
Mandrenkov May 21, 2021
a6cc282
Clarify that top-level makefile is for C++ code
Mandrenkov May 21, 2021
a75f654
Rename Python bindings package to 'bindings'
Mandrenkov May 21, 2021
09a01c0
Move Python bindings tests to subdirectory
Mandrenkov May 21, 2021
471c54a
Update import path to Python bindings module
Mandrenkov May 21, 2021
4d495e1
Rename 'requirements_test.txt' to 'requirements.txt'
Mandrenkov May 21, 2021
a8263ad
Add Python wheel dependencies
Mandrenkov May 21, 2021
1f3c921
Add Python distribution directory to .gitignore
Mandrenkov May 21, 2021
b4d59ad
Create top-level Jet Python package
Mandrenkov May 21, 2021
42d13f0
Create setup.py to build quantum-jet Python wheels
Mandrenkov May 21, 2021
5e09908
Move setup.py to python/ directory
Mandrenkov May 25, 2021
2d08472
Make cleaned directory prefixes consistent
Mandrenkov May 25, 2021
8c5475a
Add 'build' help documentation and clean build/ and dist/
Mandrenkov May 25, 2021
97eed02
Change source directory to parent directory
Mandrenkov May 25, 2021
05b7c08
Import all bindings directly into the 'jet' package
Mandrenkov May 25, 2021
95685fd
Exclude tests from package
Mandrenkov May 25, 2021
71b6622
Remove dependency on 'lark'
Mandrenkov May 25, 2021
01ae9b9
Fix wheel build error for lark
Mandrenkov May 25, 2021
b292e52
Remove deprecated Python bindings build steps
Mandrenkov May 25, 2021
c102416
Update changelog
Mandrenkov May 25, 2021
c86b04a
Adjust PR number
Mandrenkov May 25, 2021
6218b5a
Flatten jet.bindings package and apply formatter
Mandrenkov May 25, 2021
477f548
Change 'lark' requirement to 'lark-parser'
Mandrenkov May 25, 2021
17da911
add circuit class
thisac May 27, 2021
48b82b7
add first gate
thisac May 27, 2021
97a31f7
Merge branch 'main' into xir-interface-gates
thisac May 27, 2021
bf59af7
Add gates (WIP)
thisac May 28, 2021
5ee13e4
Merge branch 'xir-interface-gates' of github.com:XanaduAI/jet into xi…
thisac May 28, 2021
6bc04b8
add gates
thisac May 28, 2021
9c99b6a
Merge branch 'main' into xir-interface-gates
Mandrenkov May 31, 2021
19e7cc0
Add jet/ to formatted directories
Mandrenkov Jun 1, 2021
a1ba8ff
Refactor Gate class
Mandrenkov Jun 1, 2021
5445412
Apply formatter to jet/ directory
Mandrenkov Jun 1, 2021
0fd4c5b
Factor parameter validation to Gate class
Mandrenkov Jun 1, 2021
dc54995
Remove positional arguments after keyword arguments
Mandrenkov Jun 1, 2021
aab1c7c
Merge branch 'main' into xir-interface-gates
Mandrenkov Jun 1, 2021
aac1fab
Move dtype factories to separate module to avoid circular dependency
Mandrenkov Jun 2, 2021
e878297
Add unit tests for the Gate class
Mandrenkov Jun 2, 2021
24f0ba7
Delegate package exports to modules
Mandrenkov Jun 2, 2021
a26448e
Update name of validation parameter
Mandrenkov Jun 2, 2021
08c7975
Add unit tests for H, CNOT, and Pauli gates
Mandrenkov Jun 2, 2021
a99a0b0
Add unit tests for remaining gates
Mandrenkov Jun 2, 2021
0604c8b
Add unit test IDs
Mandrenkov Jun 3, 2021
56c6ac5
Add unit tests for CV Fock gates
Mandrenkov Jun 3, 2021
a389e28
Defer circuit.py to future PR
Mandrenkov Jun 3, 2021
1c7edff
Update changelog
Mandrenkov Jun 3, 2021
1f833d1
Add thewalrus to package requirements and specify numpy versions
Mandrenkov Jun 3, 2021
7d1e281
Create type aliases for bound classes
Mandrenkov Jun 4, 2021
033dba6
Expose num_wires parameter and move dtype to tensor() in Gate class
Mandrenkov Jun 4, 2021
a1e701f
Add Python classes to model quantum states
Mandrenkov Jun 4, 2021
2a997c1
Add Python class to model quantum circuits
Mandrenkov Jun 4, 2021
2722053
Create type aliases for union of template class specializations
Mandrenkov Jun 4, 2021
65d81a8
Convert Gate into an ABC, expose num_wires, and distribute validation
Mandrenkov Jun 4, 2021
4bd800e
Merge branch 'main' into xir-interface-gates
Mandrenkov Jun 4, 2021
4119651
Merge branch 'xir-interface-gates' into xir-interface-circuit
Mandrenkov Jun 4, 2021
db43ea3
Use MockGate instead of CPhaseShift in Gate tests
Mandrenkov Jun 4, 2021
82bfff8
Merge branch 'xir-interface-gates' into xir-interface-circuit
Mandrenkov Jun 4, 2021
f05897f
Add tests for the State, Qubit, and Qudit classes
Mandrenkov Jun 4, 2021
6ad309b
Refactor assertions to raise ValueErrors instead
Mandrenkov Jun 4, 2021
fb10633
Fix typo in Gate exception message
Mandrenkov Jun 4, 2021
b74c143
Add QuditRegister state model
Mandrenkov Jun 4, 2021
9c42cd7
Add unit tests for QuditRegister and state (in)equality
Mandrenkov Jun 4, 2021
0ceb250
Add unit tests for the Wire and Circuit classes
Mandrenkov Jun 4, 2021
748d523
Convert Qubit class into a constructor function
Mandrenkov Jun 7, 2021
9e95792
Update circuit tests to remove redundant specification of qubit dimen…
Mandrenkov Jun 7, 2021
7e3b55b
Fix typos in comments
Mandrenkov Jun 7, 2021
8b349d4
Rename 'gates.py' to 'gate.py'
Mandrenkov Jun 7, 2021
91a0cb4
Update changelog
Mandrenkov Jun 7, 2021
0750650
Sort typing imports
Mandrenkov Jun 7, 2021
e4467fc
Set PR number in changelog
Mandrenkov Jun 7, 2021
bcf086d
Elaborate docstring comments
Mandrenkov Jun 8, 2021
686c462
Replace conjugate transpose with matrix inverse
Mandrenkov Jun 8, 2021
ccfb710
Replace MockGate data with invertible matrix
Mandrenkov Jun 8, 2021
c68c08b
Move parameter validation to gate constructors
Mandrenkov Jun 8, 2021
886a513
Rename 'gates.py' to 'gate.py'
Mandrenkov Jun 8, 2021
71ac471
Apply functools.wraps()
Mandrenkov Jun 8, 2021
1906e81
Specify types of vacuum state vectors
Mandrenkov Jun 8, 2021
1485be7
Merge branch 'xir-interface-gates' into xir-interface-circuit
Mandrenkov Jun 8, 2021
59257e8
Add types to State constructor docstring
Mandrenkov Jun 8, 2021
55af49d
Reword @indices.getter docstring
Mandrenkov Jun 8, 2021
bf18d71
Merge branch 'xir-interface-gates' into xir-interface-circuit
Mandrenkov Jun 8, 2021
e7f9a03
Add type information to docstrings
Mandrenkov Jun 8, 2021
c91ceb5
Remove dependency on string type of comments
Mandrenkov Jun 8, 2021
dc4e049
Expand docstrings of Wire and Circuit classes
Mandrenkov Jun 8, 2021
4335a1f
Remove leftover *params from Displacement constructor
Mandrenkov Jun 8, 2021
4cf73bc
Import symbols from math and cmath modules explicitly
Mandrenkov Jun 8, 2021
f461881
Rename 'CNOT' gate to 'CX' gate
Mandrenkov Jun 9, 2021
e80f43a
Add aliases for NOT and CNOT gates
Mandrenkov Jun 10, 2021
c6d0e32
Merge branch 'xir-interface-gates' into xir-interface-circuit
Mandrenkov Jun 10, 2021
00b063d
Merge branch 'main' into xir-interface-circuit
Mandrenkov Jun 11, 2021
67051bf
Fix type hint for Wire.closed
Mandrenkov Jun 11, 2021
da78c9e
Replace tuples with iterators
Mandrenkov Jun 11, 2021
d2b7a0f
Improve invalid wire ID error messages
Mandrenkov Jun 11, 2021
2617d2a
Remove tensor IDs (until we know they are needed)
Mandrenkov Jun 14, 2021
c9e1a87
Merge branch 'main' into xir-interface-circuit
Mandrenkov Jun 14, 2021
3776822
Merge branch 'main' into xir-interface-circuit
Mandrenkov Jun 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### New features since last release

* Quantum circuit and state models have been added to the `jet` Python package. [(#21)](https://github.com/XanaduAI/jet/pull/21)

* Quantum gate models have been added to the `jet` Python package. [(#16)](https://github.com/XanaduAI/jet/pull/16)

* Python bindings are now available for the `TaskBasedCpuContractor` class. [(#19)](https://github.com/XanaduAI/jet/pull/19)
Expand Down
2 changes: 2 additions & 0 deletions python/jet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from .bindings import *

# The rest of the modules control their exports using `__all__`.
from .circuit import *
from .factory import *
from .gate import *
from .state import *

# Grab the current Jet version from the C++ headers.
__version__ = version()
152 changes: 152 additions & 0 deletions python/jet/circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from dataclasses import dataclass
from functools import wraps
from typing import Callable, Iterator, List, Sequence, Tuple, Union

import numpy as np

from .factory import TensorNetwork, TensorNetworkType
from .gate import Gate
from .state import Qudit, State

__all__ = [
"Wire",
"Circuit",
]


@dataclass
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
class Wire:
"""Wire represents a collection of tensor indices that are directly or
transitively associated with a qudit of a quantum circuit.
"""

# Position of the wire in the circuit.
id_: int
# Number of gates applied to this wire.
depth: int = 0
# Whether this wire has been terminated with a state.
closed: bool = False

@property
def index(self) -> str:
"""Returns the current index label of this wire."""
return f"{self.id_}-{self.depth}"


class Circuit:
mlxd marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, num_wires: int, dim: int = 2):
"""Constructs a quantum circuit. Each wire is initialized with a qudit
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
of the specified dimension in the vacuum state.

Args:
num_wires (int): number of wires in the circuit.
dim (int): dimension of each wire.
"""
self._wires = [Wire(i) for i in range(num_wires)]
self._parts = [Qudit(dim=dim) for _ in range(num_wires)]
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved

for (wire, part) in zip(self._wires, self._parts):
part.indices = [wire.index]
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor, but this logic makes me wonder if Wire and Qudit should be the same object?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not sure how that would work: a wire represents an ordered collection of tensor indices while a qudit represents a quantum state. The only commonality between the two is that qudits are placed at either end of a wire.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my confusion here stems more from the terminology. In other frameworks, Qubit/Wire/Mode are essentially synonyms describing the same object. When you see Qubit/Qudit in a codebase, I expect it to be a book-keeping object, rather than something that describes the state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's understandable. Do you think the Qudit class should be renamed to QuditState or something similar?


@property
def parts(self) -> Iterator[Union[Gate, State]]:
"""Returns the gates and states that comprise this circuit. The first
``self.num_wires`` parts are the qudits that begin each wire; other
parts appear in the order they were appended to the circuit.
"""
return iter(self._parts)

@property
def wires(self) -> Iterator[Wire]:
"""Returns the wires of this circuit in increasing order of wire ID."""
return iter(self._wires)

def _append_validator(append_fn: Callable) -> Callable:
"""Decorator which validates the arguments to an append function.

Args:
append_fn (Callable): append function to be validated.

Returns:
Function that validates the passed wire IDs before invoking the
decorated append function with the given arguments.
"""

@wraps(append_fn)
def validator(self, part: Union[Gate, State], wire_ids: Sequence[int]) -> None:
if len(wire_ids) != part.num_wires:
raise ValueError(
f"Number of wire IDs ({len(wire_ids)}) must match the number of "
f"wires connected to the circuit component ({part.num_wires})."
)

num_wires = len(self._wires)

for wire_id in wire_ids:
if not 0 <= wire_id < num_wires:
raise ValueError(f"Wire ID {wire_id} falls outside the range [0, {num_wires}).")

elif wire_ids.count(wire_id) > 1:
raise ValueError(f"Wire ID {wire_id} is specified more than once.")

elif self._wires[wire_id].closed:
raise ValueError(f"Wire {wire_id} is closed.")

append_fn(self, part, wire_ids)

return validator

@_append_validator
def append_gate(self, gate: Gate, wire_ids: Sequence[int]) -> None:
"""Applies a gate along the specified wires.

Args:
gate (Gate): gate to be applied.
wire_ids (Sequence[int]): IDs of the wires the gate is applied to.
"""
input_indices = self.indices(wire_ids)

for i in wire_ids:
self._wires[i].depth += 1

output_indices = self.indices(wire_ids)

gate.indices = output_indices + input_indices
self._parts.append(gate)

@_append_validator
def append_state(self, state: State, wire_ids: Sequence[int]) -> None:
"""Terminates the specified wires with a quantum state.

Args:
state (State): state to be used for termination.
wire_ids (Sequence[int]): IDs of the wires the state terminates.
"""
for i in wire_ids:
self._wires[i].closed = True

state.indices = self.indices(wire_ids)
self._parts.append(state)

def indices(self, wire_ids: Sequence[int]) -> List[str]:
"""Returns the index labels associated with a sequence of wire IDs.

Args:
wire_ids (Sequence[int]): IDs of the wires to get the index labels for.

Returns:
List of index labels.
"""
return [self._wires[i].index for i in wire_ids]

def tensor_network(self, dtype: type = np.complex128) -> TensorNetworkType:
mlxd marked this conversation as resolved.
Show resolved Hide resolved
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the tensor network representation of this circuit.

Args:
dtype (type): data type of the tensor network.
"""
tn = TensorNetwork(dtype=dtype)
for part in self._parts:
tensor = part.tensor(dtype=dtype)
tn.add_tensor(tensor)
return tn
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion python/jet/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def indices(self, indices: Optional[Sequence[str]]) -> None:
# Check that `indices` has the correct length.
elif len(indices) != 2 * self._num_wires:
raise ValueError(
f"Gates must have two indices per wire; received {len(indices)}"
f"Gates must have two indices per wire; received {len(indices)} "
f"indices for {self._num_wires} wires."
)

Expand Down
175 changes: 175 additions & 0 deletions python/jet/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from abc import ABC, abstractmethod
from typing import List, Optional, Sequence

import numpy as np

from .factory import Tensor, TensorType

__all__ = [
"State",
"Qudit",
"QuditRegister",
"Qubit",
"QubitRegister",
]


class State(ABC):
Mandrenkov marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, name: str, num_wires: int):
"""Constructs a quantum state.

Args:
name (str): name of the state.
num_wires (str): number of wires the state is connected to.
"""
self.name = name

self._indices = None
self._num_wires = num_wires

@property
def indices(self) -> Optional[List[str]]:
"""Returns the indices of this state. An index is a label associated with
an axis of the tensor representation of a state; the indices of a tensor
determine its connectivity in the context of a tensor network.
"""
return self._indices

@indices.setter
def indices(self, indices: Optional[Sequence[str]]) -> None:
"""Sets the indices of this state. The ``indices`` property of a state
is used to construct its tensor representation (unless ``indices`` is
None). See @indices.getter for more information about tensor indices.

Raises:
ValueError if the given indices are not a sequence of unique strings
or the number of provided indices is invalid.

Args:
indices (Sequence[str] or None): new indices of the state.
"""
# Skip the sequence property checks if `indices` is None.
if indices is None:
pass

# Check that `indices` is a sequence of unique strings.
elif (
not isinstance(indices, Sequence)
or not all(isinstance(idx, str) for idx in indices)
or len(set(indices)) != len(indices)
):
raise ValueError("Indices must be a sequence of unique strings.")

# Check that `indices` has the correct length (or is None).
elif len(indices) != self.num_wires:
raise ValueError(
f"States must have one index per wire. "
f"Received {len(indices)} indices for {self.num_wires} wires."
)

self._indices = indices

@property
def num_wires(self) -> int:
"""Returns the number of wires connected to this state."""
return self._num_wires

def __eq__(self, other) -> bool:
"""Reports whether this state is equivalent to the given state."""
return np.all(self._data() == other._data())

def __ne__(self, other) -> bool:
"""Reports whether this state is not equivalent to the given state."""
return not (self == other)

@abstractmethod
def _data(self) -> np.ndarray:
"""Returns the vector representation of this state."""
pass

def tensor(self, dtype: type = np.complex128, adjoint: bool = False) -> TensorType:
"""Returns the tensor representation of this state.

Args:
dtype (type): data type of the tensor.
adjoint (bool): whether to take the adjoint of the tensor.
"""
if adjoint:
data = np.conj(self._data())
else:
data = self._data()

indices = self.indices
if indices is None:
indices = list(map(str, range(self.num_wires)))

dimension = int(round(len(data) ** (1 / len(indices))))
shape = [dimension] * len(indices)

return Tensor(indices=indices, shape=shape, data=data, dtype=dtype)


class Qudit(State):
def __init__(self, dim: int, data: Optional[np.ndarray] = None):
"""Constructs a qudit state.

Args:
dim (int): dimension of the qudit.
data (np.ndarray or None): optional state vector.
"""
name = "Qubit" if dim == 2 else f"Qudit(d={dim})"
super().__init__(name=name, num_wires=1)

if data is None:
self._state_vector = (np.arange(dim) == 0).astype(np.complex128)
else:
self._state_vector = data.flatten()

def _data(self) -> np.ndarray:
return self._state_vector


class QuditRegister(State):
def __init__(self, dim: int, size: int, data: Optional[np.ndarray] = None):
"""Constructs a qudit register state.

Args:
dim (int): dimension of the qudits.
size (int): number of qudits.
data (np.ndarray or None): optional state vector.
"""
name = f"Qubit[{size}]" if dim == 2 else f"Qudit(d={dim})[{size}]"
super().__init__(name=name, num_wires=size)

if data is None:
self._state_vector = (np.arange(dim ** size) == 0).astype(np.complex128)
else:
self._state_vector = data.flatten()

def _data(self) -> np.ndarray:
return self._state_vector


def Qubit(data: Optional[np.ndarray] = None) -> Qudit:
"""Constructs a qubit state using an optional state vector.

Args:
data (np.ndarray or None): optional state vector.

Returns:
Qudit instance constructed using the specified state vector.
"""
return Qudit(dim=2, data=data)


def QubitRegister(size: int, data: Optional[np.ndarray] = None) -> QuditRegister:
"""Constructs a qubit register state with the given size and optional state vector.

Args:
size (int): number of qubits.
data (np.ndarray or None): optional state vector.

Returns:
QuditRegister instance constructed using the specified state vector.
"""
return QuditRegister(dim=2, size=size, data=data)
Loading