Getting Started with Unitary

The Unitary library enables a developer to create games based on quantum computing concepts.

This API is under active design and development and is subject to change, perhaps radically, without notice.

Installation

Unitary is not available as a PyPI package. You can either clone the repository and install the library from source, or install the library by using pip directly.

Install from source

  1. Clone the repository.
git clone <a href="https://github.com/quantumlib/unitary.git">https://github.com/quantumlib/unitary.git</a>
  1. Change directory to the source code.
cd unitary/
  1. Run pip install in unitary/
pip install .

Install through pip

Before you begin: create a virtual environment for your project

Run pip install and use the git+ option to pull the source code from GitHub.

pip install --quiet git+<a href="https://github.com/quantumlib/unitary.git">https://github.com/quantumlib/unitary.git</a>

If you’re using Google Colab, add an exclamation mark before your pip command:

pip install --quiet git+https://github.com/quantumlib/unitary.git

Start using Unitary

Unitary is now ready for you to use. Import the alpha library into the relevant source file.

import unitary.alpha as alpha

Create a quantum object

A Quantum Object represents an object in a game that can have a quantum state. Almost all objects have some state: position, color, orientation in game space, and so on. Some game objects can change their state during game play. A quantum state means that the classical state of the object (e.g. Color.GREEN) is uncertain until the game needs to use the classical state. At that point we observe (measure) the quantum state to retrieve a classical state.

For example, let’s create a 5x5 game board, 25 squares in total. Each square can have one of two states: empty or full. If we use quantum objects to create our game board squares, we can enable richer game play by deferring the decision about whether a piece occupies a square or not until the game needs to generate a score.

You can use an enumeration to easily track the state of each square.

5x5 game board

import enum

class Square(enum.Enum):
  EMPTY=0
  FULL=1

A QuantumObject requires a name and an initial state.

example_square = alpha.QuantumObject('b1', Square.EMPTY)

Now let's create a collection of quantum objects to represent our game board. An empty game board isn't very useful, so we also populate our board with a single row of tokens at the near edge (rank 1) and far edge (rank 5).

5x5 game board with pieces

board = {}
for col in "abcde":
  for rank in "234":
    square_name = col + rank
    board[square_name] = alpha.QuantumObject(square_name, Square.EMPTY)
  for rank in "15":
    square_name = col + rank
    board[square_name] = alpha.QuantumObject(square_name, Square.FULL)

Create a Quantum World

A Quantum World is a container that enables you to define a scope for Quantum Objects. All quantum objects in your Quantum World have the opportunity to interact with each other. The scope enables us to reason about the probability of a Quantum Object having one state versus another.

A QuantumObject must belong to a QuantumWorld object before you can use it. A QuantumObject can only belong to a single QuantumWorld.

Let’s create a QuantumWorld object to hold our game board.

game_board = alpha.QuantumWorld(board.values())

Now you can see the status of the Quantum Objects in a QuantumWorld by using the peek method.

print("Square a1 is the near edge-square; it should be full:")
print(game_board.peek([board["a1"]]))
print("Square c3 is the center square on the board; it should be empty:")
print(game_board.peek([board["c3"]]))
Square a1 is the near edge-square; it should be full:
[[<Square.FULL: 1>]]
Square c3 is the center square on the board; it should be empty:
[[<Square.EMPTY: 0>]]
print(game_board.peek())
[[<Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>, <Square.FULL: 1>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>, <Square.FULL: 1>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>, <Square.FULL: 1>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>, <Square.FULL: 1>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>, <Square.FULL: 1>]]

Apply Quantum Effects

When you want to change the quantum state to reflect game play, you apply a Quantum Effect to one or more Quantum Objects. For example, you can change the state of any square by using the Flip effect (to “flip” the bit, or state; on to off, green to red, and so on).

flip = alpha.Flip()
flip(board["c3"])

If we peek at our game board, the c3 square should be in the Square.FULL state.

print(game_board.peek([board["c3"]]))
[[<Square.FULL: 1>]]

We could manually implement basic movement by flipping the state of our source square and our target square.

print("Square a1 and a2 before the move:")
print(game_board.peek([board["a1"], board["a2"]]))

flip(board["a1"])
flip(board["a2"])

print("Square a1 and a2 after the move:")
print(game_board.peek([board["a1"], board["a2"]]))
Square a1 and a2 before the move:
[[<Square.FULL: 1>, <Square.EMPTY: 0>]]
Square a1 and a2 after the move:
[[<Square.EMPTY: 0>, <Square.FULL: 1>]]

However, the QuantumEffects API provides a number of convenience methods for basic game actions. For example, we can do a basic move by applying the Move effect.

print("Square b1 and b2 before the move:")
print(game_board.peek([board["b1"], board["b2"]]))

move = alpha.Move()
move(board["b1"], board["b2"])

print("Square b1 and b2 after the move:")
print(game_board.peek([board["b1"], board["b2"]]))
Square b1 and b2 before the move:
[[<Square.FULL: 1>, <Square.EMPTY: 0>]]
Square b1 and b2 after the move:
[[<Square.EMPTY: 0>, <Square.FULL: 1>]]

Retrieve the classical state

Similarly, you can use a Split effect to create a superposition. In our example, we can create uncertainty about the actual location of our token by creating a superposition.

Let’s create a superposition by splitting the token from square d1 to squares c2 and e2.

print("Squares d1, c2, and e2 before the split:")
print(game_board.peek([board["d1"], board["c2"], board["e2"]]))

split = alpha.Split()
split(board["d1"], board["c2"], board["e2"])
Squares d1, c2, and e2 before the split:
[[<Square.FULL: 1>, <Square.EMPTY: 0>, <Square.EMPTY: 0>]]

Before we validate whether the Split effect worked, let’s consider what we expect to see.

Each square has a 50% probability of hosting our token. When we look at our target squares, we observe their classical state at the moment that we observe them, not a probability. If you peek at one or both squares once, you see a definite classical state.

print("Squares d1, c2, and e2 after the split:")
print(game_board.peek([board["d1"], board["c2"], board["e2"]]))
Squares d1, c2, and e2 after the split:
[[<Square.EMPTY: 0>, <Square.EMPTY: 0>, <Square.FULL: 1>]]

If you peek again, you might see a different state (or, you might see the same state).

print("Squares c2 and e2 again:")
print(game_board.peek([board["c2"], board["e2"]], count=10))
Squares c2 and e2 again:
[[<Square.FULL: 1>, <Square.EMPTY: 0>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.FULL: 1>, <Square.EMPTY: 0>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>], [<Square.EMPTY: 0>, <Square.FULL: 1>]]

The more often you peek at the squares, the closer you get to observing the token in each square about half the time. That’s how we calculate the probability of the outcomes.

print("Square c2, observed 100 times:")
print(game_board.peek([board["c2"]], 100))
Square c2, observed 100 times:
[[<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.EMPTY: 0>], [<Square.FULL: 1>], [<Square.FULL: 1>]]

You can use the get_histogram method to retrieve a histogram of the states of any QuantumObject in your QuantumWorld.

print("Histogram of square c2:")
print(game_board.get_histogram([board["c2"]]))

print("Histogram of square c2 and e2:")
print(game_board.get_histogram([board["c2"]]), game_board.get_histogram([board["e2"]]))
Histogram of square c2:
[{0: 44, 1: 56}]
Histogram of square c2 and e2:
[{0: 46, 1: 54}] [{0: 48, 1: 52}]

By default, the get_histogram method samples your QuantumWorld 100 times. You can use the count option to change the number of samples that get_histogram uses to calculate the probability distribution.

When you’re ready to use the classical state, use the pop method to retrieve the classical state from your QuantumWorld. After you pop the classical state, subsequent calls to the peek method gives you a stable answer.

print("Final state of square c2 and e2:")
game_board.pop([board["c2"], board["e2"]])

print("Histogram of square c2, after a call to `pop`:")
print(game_board.get_histogram([board["c2"]], count=10))
Final state of square c2 and e2:
Histogram of square c2, after a call to `pop`:
[{0: 0, 1: 10}]

Control flow in quantum and classical states separately

You cannot control the flow of your quantum state from your classical game logic, and vice versa. However, Unitary provides a quantum_if method to modify the state of one QuantumObject based on the state of another.

For an example, we start with a scenario where we know the outcome, then explore a scenario where the outcome is conditional.

Before we begin, let’s destroy the token on c3 that we created earlier. Set the square to the Square.EMPTY state.

flip(board["c3"])

On our board, let’s assume a rule that a token can slide to another square only when the path is empty. If we move the token from b5 to b4, the token on a5 cannot reach square c3 (blocked by token on b4).

move(board["b5"], board["b4"])

alpha.quantum_if(board["b4"]).equals(Square.EMPTY).apply(move)(board["a5"], board["c3"])

print(game_board.peek([board["a5"]]))
print(game_board.peek([board["b4"]]))
print(game_board.peek([board["c3"]]),100)
[[<Square.FULL: 1>]]
[[<Square.FULL: 1>]]
[[<Square.EMPTY: 0>]] 100

Let’s move the token on b4 back to b5, then try again.

move(board["b4"], board["b5"])

alpha.quantum_if(board["b4"]).equals(Square.EMPTY).apply(move)(board["a5"], board["c3"])

print(game_board.peek([board["a5"]]))
print(game_board.peek([board["b4"]]))
print(game_board.peek([board["c3"]],100))
[[<Square.EMPTY: 0>]]
[[<Square.EMPTY: 0>]]
[[<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>], [<Square.FULL: 1>]]

The token on the B file was in a classical state, so it blocked the progress of the token on the A file. After we removed the token on the B file, the token on the A-file was free to move.

Let’s put the token on the B file in a superposition, and observe what happens to the token on the A file.

split(board["b5"], board["b4"], board["c4"])

alpha.quantum_if(board["b4"]).equals(Square.EMPTY).apply(move)(board["a5"], board["c3"])

print(game_board.peek([board["a5"]]))
print(game_board.get_histogram([board["b4"]]))
print(game_board.get_histogram([board["c3"]]))
[[<Square.FULL: 1>]]
[{0: 44, 1: 56}]
[{0: 50, 1: 50}]

By passing a token in a classical state (a5) through a token in a quantum state (b4), we entangled the classical token and made its outcome quantum. When you pop the classical state of the b4/c4 token, the Quantum World also calculates the classical state of the a5/c3 token. You find the token on a5 if the b4/c4 token is on b4, or on c3 if the b4/c4 token is on c4.

Note that the apply method only accepts a QuantumEffect object. You cannot provide classical control flow to the quantum state.