Qx is a quantum computing simulator built for Elixir that provides an intuitive API for creating and simulating quantum circuits. The primary goal of the project is to enhance my understanding of quantum computing concepts, quantum simulators and the Elixir Nx library. My hope is that it is eventualy valuable for others to learn quantum computing. It supports up to 20 qubits (an arbitrary number that I feel is useful but still below the memory cliff that would occurs around 30 qubits).
- Two Modes of Operation:
- Circuit Mode: Build quantum circuits and execute them (traditional workflow)
- Calculation Mode: Apply gates in real-time and inspect states immediately (great for learning!)
- Simple API: Easy-to-use functions for quantum circuit creation and simulation
- Up to 20 Qubits: Supports quantum circuits with up to 20 qubits
- Statevector Simulation: Uses statevector method for accurate quantum state representation
- Optional Acceleration: Add EXLA or EMLX backends for speedup (CPU/GPU)
- Visualization: Built-in plotting capabilities with SVG and VegaLite support, plus circuit diagram generation
- Growing Range of Gates: Supports H, X, Y, Z, S, T, RX, RY, RZ, CNOT, CZ, and Toffoli gates
- Measurements: Quantum measurements with classical bit storage
- Conditional Operations: Mid-circuit measurement with classical feedback for quantum processes like teleportation and error correction
- LiveBook Integration: Full support with interactive visualizations in LiveBook
Qx works immediately on any platform without additional acceleration libraries:
def deps do
[
{:qx_sim, "~> 0.3.0"}
]
endThen run:
mix deps.getOr install from GitHub for the latest development version:
def deps do
[
{:qx_sim, github: "richarc/qx", branch: "main"}
]
endThis installs Qx with the default Nx.BinaryBackend, which works on all platforms but is slower for larger quantum circuits (10+ qubits).
Want better performance? See Performance & Acceleration below to add optional EXLA (CPU/GPU) or EMLX (Apple Silicon GPU) for backend speedup.
Qx works out-of-the-box with Nx.BinaryBackend on all platforms, but you can add acceleration backends for significant speedups, especially for circuits with 10+ qubits.
| Backend | Platform | Compilation Required |
|---|---|---|
| Nx.BinaryBackend | All | No (default) |
| EXLA (CPU) | All | Yes (C++ compiler needed) |
| EXLA (CUDA) | Linux/Windows + NVIDIA GPU | Yes + CUDA Toolkit |
| EXLA (ROCm) | Linux + AMD GPU | Yes + ROCm |
| EMLX (Metal) | macOS Apple Silicon | No (precompiled) |
Select the option that matches your platform and needs:
- EXLA CPU (All Platforms) ← Recommended for most users
- EXLA + NVIDIA GPU (Linux/Windows) ← For NVIDIA GPU acceleration
- EXLA + AMD GPU (Linux) ← For AMD GPU acceleration
- EMLX + Apple Silicon GPU (macOS) ← For M1/M2/M3/M4 Macs
Best for: All platforms • No GPU required
EXLA provides significant speedup through XLA's LLVM optimizations without requiring GPU hardware.
macOS:
# Install Xcode Command Line Tools
xcode-select --installLinux (Debian/Ubuntu):
sudo apt install build-essentialLinux (Fedora/RHEL):
sudo dnf groupinstall "Development Tools"Windows:
- Install Visual Studio Build Tools with C++ support, OR
- Use WSL2 (recommended)
Edit your mix.exs:
def deps do
[
{:qx_sim, "~> 0.3.0"},
{:exla, "~> 0.10"} # Add this line
]
endmix deps.getNote: First-time EXLA compilation takes several minutes. See EXLA installation guide if compilation fails.
Create or edit config/config.exs:
import Config
# Use EXLA with CPU
config :nx, :default_backend, EXLA.Backendiex -S mixiex> Nx.default_backend()
EXLA.Backend
iex> # Test with a quantum circuit
iex> Qx.create_circuit(10, 0) |> Qx.h(0) |> Qx.get_state()
# Should execute quickly with EXLABest for: Linux/Windows with NVIDIA GPU
Provides massive acceleration for larger quantum circuits using CUDA.
Download and install CUDA Toolkit 11.8 or 12.0:
Verify installation:
nvcc --versionYou should see output like: Cuda compilation tools, release 11.8 or release 12.0
Add to your shell profile (~/.bashrc, ~/.zshrc, or ~/.bash_profile):
# For CUDA 11.x
export XLA_TARGET=cuda118
# For CUDA 12.x
export XLA_TARGET=cuda120Then reload your shell:
source ~/.bashrc # or source ~/.zshrcEdit your mix.exs:
def deps do
[
{:qx_sim, "~> 0.3.0"},
{:exla, "~> 0.10"} # Add this line
]
endmix deps.getNote: EXLA will compile with CUDA support (15-45 minutes on first install).
Create or edit config/config.exs:
import Config
# Use EXLA with CUDA GPU
config :nx, :default_backend, {EXLA.Backend, client: :cuda}iex -S mixiex> Nx.default_backend()
{EXLA.Backend, [client: :cuda]}
iex> EXLA.Client.get_supported_platforms()
# Should show :cuda in the list
iex> # Check GPU is detected
iex> :cuda in EXLA.Client.get_supported_platforms()
true- "CUDA not found": Ensure
XLA_TARGETenvironment variable is set correctly (check withecho $XLA_TARGET) - Compilation fails: Verify CUDA toolkit version matches
XLA_TARGETvalue - Runtime errors: Update NVIDIA drivers to latest version (
nvidia-smito check current version) - Out of memory: Reduce circuit size or qubit count
Best for: Linux with AMD GPU
Provides similar acceleration to CUDA for AMD GPUs on Linux.
Follow the official installation guide for your Linux distribution:
Minimum version: ROCm 5.4 or later
Verify installation:
rocm-smiEdit your mix.exs:
def deps do
[
{:qx_sim, "~> 0.3.0"},
{:exla, "~> 0.10"} # Add this line
]
endmix deps.getNote: EXLA will compile with ROCm support (Several minutes on first install).
Create or edit config/config.exs:
import Config
# Use EXLA with ROCm GPU
config :nx, :default_backend, {EXLA.Backend, client: :rocm}iex -S mixiex> Nx.default_backend()
{EXLA.Backend, [client: :rocm]}
iex> EXLA.Client.get_supported_platforms()
# Should show :rocm in the listBest for: macOS M1/M2/M3/M4 • No compilation required
Note: EXLA does not support Metal GPU acceleration. For Apple Silicon GPU acceleration, use EMLX. For CPU-only acceleration on Apple Silicon, use EXLA CPU instead.
EMLX provides Metal GPU acceleration through Apple's MLX framework, designed specifically for Apple's unified memory architecture.
Edit your mix.exs:
def deps do
[
{:qx_sim, "~> 0.3.0"},
{:emlx, github: "elixir-nx/emlx", branch: "main"} # Add this line
]
endmix deps.getNote: EMLX automatically downloads precompiled MLX binaries (no compilation needed).
Create or edit config/config.exs:
import Config
# Use EMLX with Metal GPU
config :nx, :default_backend, {EMLX.Backend, device: :gpu}iex -S mixiex> Nx.default_backend()
{EMLX.Backend, [device: :gpu]}
iex> # Test with a simple tensor
iex> Nx.tensor([1, 2, 3]) |> IO.inspect()
# Should show EMLX backend in use- Metal does not support 64-bit floats, but Qx uses Complex64 which is fully supported
- EMLX downloads precompiled binaries, so no C++ compiler is needed
- For CPU-only acceleration on Apple Silicon, use EXLA CPU instead (requires compilation but works without GPU)
Starting with Qx v0.3.0, you can select backends at runtime without compile-time configuration. This is useful for:
- Testing different backends without recompiling
- Using different backends for different circuits in the same application
- Overriding the default backend for specific operations
# Create a circuit
qc = Qx.create_circuit(10) |> Qx.h(0) |> Qx.cx(0, 1)
# Run with EXLA backend (even if binary backend is default)
result = Qx.run(qc, backend: EXLA.Backend)
# Run with EXLA + CUDA
result = Qx.run(qc, backend: {EXLA.Backend, client: :cuda})
# Run with EMLX on Apple Silicon
result = Qx.run(qc, backend: {EMLX.Backend, device: :gpu})
# Combine with other options
result = Qx.run(qc, backend: EXLA.Backend, shots: 2048)The :backend option works with all simulation functions:
# Get final state with specific backend
state = Qx.get_state(qc, backend: EXLA.Backend)
# Get probabilities with specific backend
probs = Qx.get_probabilities(qc, backend: EXLA.Backend)Runtime backend selection (:backend option):
- ✅ Best for applications that need flexibility
- ✅ No recompilation needed to switch backends
- ✅ Can use different backends for different circuits
- ✅ Great for testing and benchmarking
Compile-time configuration (config/config.exs):
- ✅ Sets a project-wide default
- ✅ No need to specify backend on every call
- ✅ Traditional approach, widely documented
You can combine both approaches: set a default in config/config.exs and override it at runtime with the :backend option when needed.
# Create and manipulate qubits directly - gates apply immediately!
q = Qx.Qubit.new()
|> Qx.Qubit.h()
Qx.Qubit.show_state(q)
# Output:
# %{
# state: "0.707|0⟩ + 0.707|1⟩",
# amplitudes: [{"|0⟩", "0.707+0.000i"}, {"|1⟩", "0.707+0.000i"}],
# probabilities: [{"|0⟩", 0.5}, {"|1⟩", 0.5}]
# }
# Inspect state at any step
q = Qx.Qubit.new()
Qx.Qubit.measure_probabilities(q) # [1.0, 0.0] - definitely |0⟩
q = Qx.Qubit.x(q)
Qx.Qubit.measure_probabilities(q) # [0.0, 1.0] - definitely |1⟩
q = Qx.Qubit.h(q)
Qx.Qubit.measure_probabilities(q) # [0.5, 0.5] - superposition!# Create a Bell state (maximally entangled two-qubit state)
result = Qx.bell_state() |> Qx.run()
# Visualize the results
Qx.draw(result)# Create a circuit with 2 qubits and 2 classical bits
qc = Qx.create_circuit(2, 2)
|> Qx.h(0) # Apply Hadamard gate to qubit 0
|> Qx.cx(0, 1) # Apply CNOT gate (control: 0, target: 1)
|> Qx.measure(0, 0) # Measure qubit 0, store in classical bit 0
|> Qx.measure(1, 1) # Measure qubit 1, store in classical bit 1
# Run the simulation
result = Qx.run(qc, 1000) # 1000 measurement shots
# Display results
IO.inspect(result.counts)LiveBook is the perfect environment for interactive quantum computing with Qx!
Create a new LiveBook notebook and add this in the 'setup' cell:
Mix.install([
{:qx, "~> 0.3.0", hex: :qx_sim},
{:kino, "~> 0.12"},
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.11"}
])This works immediately on all platforms without compilation or acceleration libraries. Best for small circuits (< 10 qubits) and learning.
For better performance with larger circuits, choose the setup that matches your platform:
Mix.install([
{:qx, "~> 0.3.0", hex: :qx_sim},
{:exla, "~> 0.10"},
{:kino, "~> 0.12"},
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.11"}
])
# Configure EXLA backend
Application.put_env(:nx, :default_backend, EXLA.Backend)Prerequisites: See EXLA CPU setup for installing C++ compiler.
Mix.install([
{:qx, "~> 0.3.0", hex: :qx_sim},
{:emlx, github: "elixir-nx/emlx", branch: "main"},
{:kino, "~> 0.12"},
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.11"}
])
# Configure for Metal GPU
Application.put_env(:nx, :default_backend, {EMLX.Backend, device: :gpu})Prerequisites:
- Install CUDA Toolkit (see NVIDIA GPU setup)
- Set
export XLA_TARGET=cuda118(orcuda120) in your shell profile - Restart your terminal/shell
Mix.install([
{:qx, "~> 0.3.0", hex: :qx_sim},
{:exla, "~> 0.10"},
{:kino, "~> 0.12"},
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.11"}
])
# Configure for CUDA GPU
Application.put_env(:nx, :default_backend, {EXLA.Backend, client: :cuda})Prerequisites: Install ROCm (see AMD GPU setup)
Mix.install([
{:qx, "~> 0.3.0", hex: :qx_sim},
{:exla, "~> 0.10"},
{:kino, "~> 0.12"},
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.11"}
])
# Configure for ROCm GPU
Application.put_env(:nx, :default_backend, {EXLA.Backend, client: :rocm})Once set up, you can create beautiful interactive visualizations:
# Create a Bell state
circuit = Qx.create_circuit(2, 2)
|> Qx.h(0)
|> Qx.cx(0, 1)
|> Qx.measure(0, 0)
|> Qx.measure(1, 1)
# Run simulation
result = Qx.run(circuit, 1000)
# Visualize with Kino
Qx.draw_counts(result)LiveBook's reactive cells make quantum state exploration intuitive:
# Calculation Mode - perfect for learning!
import Qx.Qubit
# Create a qubit and apply Hadamard gate
qubit = new() |> h()
# Display the state
show_state(qubit) |> Kino.render()
# Apply more gates and see immediate results
qubit
|> x()
|> show_state()
|> Kino.render()Check that EXLA is active:
# Verify backend
IO.inspect(Nx.default_backend(), label: "Active Backend")
# Run a quick benchmark
{time, _result} = :timer.tc(fn ->
Qx.create_circuit(15, 0)
|> Qx.h(0)
|> Qx.cx(0, 1)
|> Qx.cx(1, 2)
|> Qx.get_state()
end)
IO.puts("15-qubit circuit execution: #{time / 1000} ms")
# Should see ~90ms with EXLA, vs 15+ seconds without-
Start Simple: Begin with the basic setup (no acceleration) for learning and small circuits, then add acceleration when needed
-
Use Calculation Mode for Learning: Real-time gate application with
Qx.QubitandQx.Registeris perfect for understanding quantum mechanics interactively -
Leverage Kino Widgets: Use
Kino.render()to create interactive controls for gate parameters -
Performance: Add EXLA or EMLX to your Mix.install for better performance with larger circuits (10+ qubits)
-
Visualization:
Qx.draw_counts/1returns VegaLite specs that render beautifully in LiveBook -
Debugging: Use tap functions (
tap_state,tap_probabilities) in pipelines withIO.inspectfor immediate feedback
Check out example notebooks in the repository:
examples/livebook/getting_started.livemd- Basic introductionexamples/livebook/quantum_teleportation.livemd- Complete teleportation tutorialexamples/livebook/grovers_algorithm.livemd- Search algorithm implementation
The 'Qx' module implements a handy API for the majority of functions needed to create simple quantum circuits. It is a series of delegations to the following modules:
Qx.Qubit- Calculation mode: Define and manipulate individual qubits in real-timeQx.Register- Calculation mode: Multi-qubit registers with entanglementQx.QuantumCircuit- Circuit mode: Structure and functions for quantum circuitsQx.Operations- Gate operations on circuitsQx.Simulation- Simulation and execution of circuits
Work with qubits directly - gates apply immediately!
Qubit Creation:
Qx.Qubit.new()- Create |0⟩ stateQx.Qubit.new(alpha, beta)- Create custom state α|0⟩ + β|1⟩Qx.Qubit.one()- Create |1⟩ stateQx.Qubit.plus()- Create |+⟩ stateQx.Qubit.minus()- Create |-⟩ stateQx.Qubit.from_basis(0 | 1)- Create from computational basisQx.Qubit.from_bloch(theta, phi)- Create from Bloch sphere coordinatesQx.Qubit.from_angle(theta)- Create from angle (simplified Bloch sphere)
Single-Qubit Gates (Calculation Mode):
Qx.Qubit.h/1- Hadamard gateQx.Qubit.x/1- Pauli-X gateQx.Qubit.y/1- Pauli-Y gateQx.Qubit.z/1- Pauli-Z gateQx.Qubit.s/1- S gateQx.Qubit.t/1- T gateQx.Qubit.rx/2- X-rotationQx.Qubit.ry/2- Y-rotationQx.Qubit.rz/2- Z-rotationQx.Qubit.phase/2- Phase gate
State Inspection:
Qx.Qubit.state_vector/1- Get raw state tensorQx.Qubit.show_state/1- Get human-readable state (Dirac notation, amplitudes, probabilities)Qx.Qubit.measure_probabilities/1- Get measurement probabilitiesQx.Qubit.alpha/1- Get |0⟩ amplitudeQx.Qubit.beta/1- Get |1⟩ amplitude
Work with multi-qubit registers - gates apply immediately with full entanglement support!
Register Creation:
Qx.Register.new(num_qubits)- Create register with n qubits (all |0⟩)Qx.Register.new([qubit1, qubit2, ...])- Create from list of qubits via tensor productQx.Register.from_basis_states([0, 1, 0])- Create from list of basis states (e.g., |010⟩)Qx.Register.from_superposition(n)- Create n-qubit register in equal superposition
Single-Qubit Gates (on specific qubits):
Qx.Register.h(register, qubit_index)- Hadamard gateQx.Register.x(register, qubit_index)- Pauli-X gateQx.Register.y(register, qubit_index)- Pauli-Y gateQx.Register.z(register, qubit_index)- Pauli-Z gateQx.Register.s(register, qubit_index)- S gateQx.Register.t(register, qubit_index)- T gateQx.Register.rx(register, qubit_index, theta)- X-rotationQx.Register.ry(register, qubit_index, theta)- Y-rotationQx.Register.rz(register, qubit_index, theta)- Z-rotationQx.Register.phase(register, qubit_index, phi)- Phase gate
Multi-Qubit Gates:
Qx.Register.cx(register, control, target)- CNOT gateQx.Register.cz(register, control, target)- Controlled-Z gateQx.Register.ccx(register, control1, control2, target)- Toffoli gate
State Inspection:
Qx.Register.state_vector(register)- Get full state vectorQx.Register.get_probabilities(register)- Get measurement probabilities for all basis statesQx.Register.show_state(register)- Get human-readable multi-qubit state representationQx.Register.valid?(register)- Check if register is properly normalized
Circuit Creation:
Qx.create_circuit(num_qubits)- Create circuit with only qubitsQx.create_circuit(num_qubits, num_classical_bits)- Create circuit with qubits and classical bits
Single-Qubit Gates (Circuit Mode):
Qx.h(circuit, qubit)- Hadamard gate (creates superposition)Qx.x(circuit, qubit)- Pauli-X gate (bit flip)Qx.y(circuit, qubit)- Pauli-Y gateQx.z(circuit, qubit)- Pauli-Z gate (phase flip)Qx.s(circuit, qubit)- S gate (phase gate π/2)Qx.t(circuit, qubit)- T gate (phase gate π/4)
Qx.rx(circuit, qubit, theta)- Rotation around X-axisQx.ry(circuit, qubit, theta)- Rotation around Y-axisQx.rz(circuit, qubit, theta)- Rotation around Z-axisQx.phase(circuit, qubit, phi)- Phase gate with custom angle
Qx.cx(circuit, control, target)- CNOT gateQx.cz(circuit, control, target)- Controlled-Z gateQx.ccx(circuit, control1, control2, target)- Toffoli gate (CCNOT)
Qx.measure(circuit, qubit, classical_bit)- Measure qubit and store result
Qx.c_if(circuit, classical_bit, value, gate_fn)- Apply gates conditionally based on classical bit value
Enables quantum error correction, quantum teleportation, and adaptive algorithms through mid-circuit measurements with classical feedback.
Example:
# Quantum teleportation with conditional corrections
qc = Qx.create_circuit(3, 3)
|> Qx.x(0) # State to teleport
|> Qx.h(1) |> Qx.cx(1, 2) # Create Bell pair
|> Qx.cx(0, 1) |> Qx.h(0) # Bell measurement
|> Qx.measure(0, 0)
|> Qx.measure(1, 1)
# Conditional corrections based on measurement
|> Qx.c_if(1, 1, fn c -> Qx.x(c, 2) end)
|> Qx.c_if(0, 1, fn c -> Qx.z(c, 2) end)
|> Qx.measure(2, 2)
result = Qx.run(qc, 1000)
# Qubit 2 now contains the teleported state!See examples/conditional_gates_example.exs for more examples.
Qx.barrier(circuit, qubits)- Add visual barrier for circuit organization
Qx.run(circuit)- Run simulation with default 1024 shots (returnsSimulationResult)Qx.run(circuit, shots)- Run simulation with specified number of shotsQx.get_state(circuit)- Get quantum state vector directly (only for circuits without measurements)Qx.get_probabilities(circuit)- Get probability distribution (only for circuits without measurements)
The Qx.run/2 function returns a SimulationResult struct with helper functions:
Qx.SimulationResult.most_frequent(result)- Get most common measurement outcomeQx.SimulationResult.filter_by_probability(result, threshold)- Filter outcomes by probabilityQx.SimulationResult.outcomes(result)- Get list of all unique outcomesQx.SimulationResult.probability(result, outcome)- Get probability of specific outcomeQx.SimulationResult.to_map(result)- Convert to map for backwards compatibility
Pipeline-friendly tap functions for inspecting circuits during construction:
Qx.tap_circuit(circuit, fn)- Inspect circuit metadata without breaking pipelineQx.tap_state(circuit, fn)- Inspect quantum state during buildingQx.tap_probabilities(circuit, fn)- Inspect measurement probabilities
Example:
result = Qx.create_circuit(2)
|> Qx.h(0)
|> Qx.tap_circuit(fn c -> IO.puts("Gates: #{length(c.instructions)}") end)
|> Qx.tap_state(&IO.inspect(&1, label: "State after H"))
|> Qx.cx(0, 1)
|> Qx.tap_probabilities(fn p -> IO.puts("Bell state created!") end)
|> Qx.run(1000)Qx provides domain-specific exception types for better error handling:
Qx.QubitIndexError- Qubit index out of rangeQx.StateNormalizationError- Invalid quantum state normalizationQx.MeasurementError- Measurement-related errorsQx.ConditionalError- Conditional operation errorsQx.ClassicalBitError- Classical bit index errorsQx.GateError- Gate operation errorsQx.QubitCountError- Invalid qubit count
Example:
try do
circuit |> Qx.h(999)
rescue
Qx.QubitIndexError -> IO.puts("Invalid qubit index!")
Qx.GateError -> IO.puts("Gate operation failed!")
endResults Visualization:
Qx.draw(result)- Plot probability distribution (VegaLite)Qx.draw(result, format: :svg)- Plot as SVGQx.draw_counts(result)- Plot measurement countsQx.histogram(probabilities)- Create probability histogram
Circuit Diagrams:
Qx.Draw.circuit(circuit)- Generate SVG circuit diagramQx.Draw.circuit(circuit, title)- Generate circuit diagram with title
Qx.bell_state()- Create Bell state circuitQx.ghz_state()- Create GHZ state circuitQx.superposition()- Create single-qubit superposition
# Create a Bell state circuit
circuit = Qx.create_circuit(2, 2)
|> Qx.h(0)
|> Qx.cx(0, 1)
|> Qx.measure(0, 0)
|> Qx.measure(1, 1)
# Generate and save circuit diagram
svg = Qx.Draw.circuit(circuit, "Bell State")
File.write!("bell_state.svg", svg)The circuit diagram feature supports:
- All quantum gates with proper IEEE notation
- Parametric gates (displays angles like RX(π/2))
- Multi-qubit gates with collision avoidance
- Barriers for visual organization
- Measurements with classical bit connections
- Publication-quality SVG output
See examples/circuit_visualization_example.exs for more examples.
# Create a quantum teleportation circuit (teleport |1⟩ state)
qc = Qx.create_circuit(3, 3)
|> Qx.x(0) # Prepare |1⟩ to teleport
|> Qx.h(1) # Create Bell pair
|> Qx.cx(1, 2) # between qubits 1 and 2
|> Qx.cx(0, 1) # Bell measurement
|> Qx.h(0)
|> Qx.measure(0, 0) # Measure qubit 0
|> Qx.measure(1, 1) # Measure qubit 1
|> Qx.c_if(1, 1, fn c -> Qx.x(c, 2) end) # Conditional corrections
|> Qx.c_if(0, 1, fn c -> Qx.z(c, 2) end)
|> Qx.measure(2, 2) # Measure teleported qubit
# Run simulation
result = Qx.run(qc, 1000)
# Analyze with new SimulationResult helpers
{most_common, count} = Qx.SimulationResult.most_frequent(result)
IO.puts("Most frequent: #{most_common} (#{count} times)")
# All outcomes should have rightmost bit = 1 (successful teleportation)
# Output: "001", "011", "101", or "111" - all with last bit = 1
# Visualize
Qx.draw_counts(result)# Simplified Grover's algorithm for 2 qubits
grover = Qx.create_circuit(2)
|> Qx.h(0) # Initialize superposition
|> Qx.h(1)
# Oracle (flip phase of target state)
|> Qx.z(0)
|> Qx.z(1)
# Diffusion operator
|> Qx.h(0)
|> Qx.h(1)
|> Qx.x(0)
|> Qx.x(1)
|> Qx.cx(0, 1)
|> Qx.x(0)
|> Qx.x(1)
|> Qx.h(0)
|> Qx.h(1)
result = Qx.run(grover)
Qx.draw(result)Circuit Mode:
# Create a 3-qubit GHZ state and examine its properties
ghz_circuit = Qx.ghz_state()
# Get the quantum state vector
state = Qx.get_state(ghz_circuit)
IO.inspect(Nx.to_flat_list(state))
# Get probabilities for all computational basis states
probs = Qx.get_probabilities(ghz_circuit)
Qx.histogram(probs)Calculation Mode (Single Qubit):
# Create and inspect qubit states in real-time
q = Qx.Qubit.new()
|> Qx.Qubit.h()
|> Qx.Qubit.z()
# Show the state
state_info = Qx.Qubit.show_state(q)
IO.puts(state_info.state) # "0.707|0⟩ - 0.707|1⟩"
IO.inspect(state_info.probabilities) # [{"|0⟩", 0.5}, {"|1⟩", 0.5}]
# From basis constructors
q = Qx.Qubit.from_basis(1) # Create |1⟩ directly
|> Qx.Qubit.h()
# Create from Bloch sphere (theta=π/2, phi=0 gives |+⟩)
q = Qx.Qubit.from_bloch(:math.pi() / 2, 0)
Qx.Qubit.show_state(q)
# Chain multiple operations
q = Qx.Qubit.new()
|> Qx.Qubit.rx(:math.pi() / 4)
|> Qx.Qubit.ry(:math.pi() / 3)
|> Qx.Qubit.rz(:math.pi() / 6)
Qx.Qubit.show_state(q)Calculation Mode (Multi-Qubit Register):
# Create a Bell state in real-time
reg = Qx.Register.new(2)
|> Qx.Register.h(0)
|> Qx.Register.cx(0, 1)
Qx.Register.show_state(reg)
# Output shows entangled state:
# %{
# state: "0.707|00⟩ + 0.707|11⟩",
# amplitudes: [{"|00⟩", "0.707+0.000i"}, {"|01⟩", "0.000+0.000i"}, ...],
# probabilities: [{"|00⟩", 0.5}, {"|01⟩", 0.0}, {"|10⟩", 0.0}, {"|11⟩", 0.5}]
# }
# Create from basis states
reg = Qx.Register.from_basis_states([0, 1, 0]) # |010⟩ state
Qx.Register.show_state(reg)
# Create in equal superposition
reg = Qx.Register.from_superposition(3) # All 8 states equally likely
Qx.Register.get_probabilities(reg)
# Create register from existing qubits
q1 = Qx.Qubit.new(0.6, 0.8) # Custom state
q2 = Qx.Qubit.plus() # |+⟩ state
reg = Qx.Register.new([q1, q2])
|> Qx.Register.h(0)
Qx.Register.get_probabilities(reg)The Qx library consists of several modules:
Qx- Main API providing convenient functionsQx.Qubit- Calculation mode: Real-time single-qubit manipulationQx.Register- Calculation mode: Multi-qubit registers with entanglementQx.QuantumCircuit- Circuit mode: Quantum circuit structure and managementQx.Operations- Quantum gate operations for circuitsQx.Simulation- Circuit execution and simulation engineQx.SimulationResult- Structured simulation results with helper functionsQx.Draw- Visualization and plotting functionsQx.Math- Core mathematical functions for quantum mechanicsQx.Validation- Input validation with custom exceptionsQx.Behaviours.QuantumState- Behaviour for consistent quantum state APIs
When to use Calculation Mode (Qx.Qubit / Qx.Register):
- Learning quantum computing concepts
- Exploring single or multi-qubit gates and states
- Creating and inspecting entangled states interactively
- Debugging quantum algorithms step-by-step
- Interactive experimentation with immediate feedback
- Immediate state inspection needed at each step
When to use Circuit Mode (Qx.create_circuit):
- Multi-shot simulations for statistics
- Measurements with classical bit storage
- Conditional operations based on measurements
- Building reusable quantum circuits
- Performance-critical batch simulations
- Exporting to OpenQASM for real hardware
These are the versions I've developed and tested with:
- Elixir 1.18+
- Nx 0.10+ (for numerical computations)
- VegaLite 0.1+ (for visualization)
For better performance, you can add:
- EXLA 0.10+ - CPU/GPU acceleration via XLA (see Performance & Acceleration)
- EMLX 0.2+ - Apple Silicon GPU acceleration via Metal (see Apple Silicon GPU setup)
Current version limitations:
- Maximum 20 qubits
- Statevector simulation only (no density matrix)
- Ideal gates only (no noise modeling)
If you've cloned the Qx repository, you can run examples directly:
# Run circuit visualization examples
mix run examples/circuit_visualization_example.exs
# Run basic usage examples
elixir examples/basic_usage.exs
# Run conditional gates examples
elixir examples/conditional_gates_example.exsIf you've installed Qx as a dependency in your project, don't run examples from deps/qx/. Instead:
# Copy example files to your project
cp deps/qx/examples/*.exs ./
# Run them from your project root
mix run circuit_visualization_example.exsAll major features have example code throughout this README that you can copy directly into:
- Your own
.exsscripts - IEx sessions (
iex -S mix) - LiveBook notebooks (see Using Qx with LiveBook)
Check out the interactive LiveBook notebooks in the repository:
examples/livebook/getting_started.livemdexamples/livebook/quantum_teleportation.livemdexamples/livebook/grovers_algorithm.livemd
Copy these to your project or open them directly in LiveBook for an interactive experience.
Run the test suite:
mix testContributions are welcome! If you'd like to contribute to Qx:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and ensure tests pass (
mix test) - Run code quality checks (
mix credo --strict) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
If you're a maintainer preparing a release, see RELEASE.md for detailed instructions on the automated release process.
This project is licensed under the Apache License 2.0.
- Built with Nx for numerical computations
- Visualization powered by VegaLite
- Inspired by quantum computing frameworks like Qiskit and Cirq
Current version: 0.3.0
For detailed API documentation, run:
mix docs