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
- Clone the repository.
git clone <a href="https://github.com/quantumlib/unitary.git">https://github.com/quantumlib/unitary.git</a>
- Change directory to the source code.
cd unitary/
- Run
pip install
inunitary/
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.
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).
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.