Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 12 additions & 12 deletions src/oqd_teaching_demo/digital.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from typing import List, Literal, Union

from pydantic import BaseModel, root_validator, NonNegativeInt
from pydantic import BaseModel, Field, model_validator, NonNegativeInt

########################################################################################

Expand All @@ -34,25 +34,25 @@ class BinaryGate(BaseModel):
control: NonNegativeInt
target: NonNegativeInt

@root_validator
def consistency_check(cls, values):
if values["control"] == values["target"]:
@model_validator(mode="after")
def consistency_check(self):
if self.control == self.target:
raise ValueError("Inconsistency: target equals control")
return values
return self


class Circuit(BaseModel):
N: Literal[5] = 5
N: int = Field(default=4, ge=1, le=8)
instructions: List[Union[UnaryGate, BinaryGate]]

@root_validator
def consistency_check(cls, values):
for gate in values["instructions"]:
if gate.target >= values["N"]:
@model_validator(mode="after")
def consistency_check(self):
for gate in self.instructions:
if gate.target >= self.N:
raise ValueError("Inconsistency: target exceeds N")
if isinstance(gate, BinaryGate) and gate.control >= values["N"]:
if isinstance(gate, BinaryGate) and gate.control >= self.N:
raise ValueError("Inconsistency: control exceeds N")
return values
return self


class Program(BaseModel):
Expand Down
115 changes: 115 additions & 0 deletions src/oqd_teaching_demo/digital_compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2024-2025 Open Quantum Design

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Compiles a digital quantum Circuit into a hardware Program (laser intensity
arrays and trap commands) for execution on the teaching demo.

Gate-to-laser mapping rationale:
In a real trapped-ion quantum computer, single-qubit gates are performed by
shining a laser on a specific ion. Different gates require different pulse
patterns. For this teaching demo, each qubit maps to one red laser, and each
gate type maps to a characteristic intensity pattern:

- X gate (bit flip): full power pulse [1.0, 1.0]
- Z gate (phase flip): half power pulse [0.5, 0.5]
- H gate (superposition): ramp pattern [0.7, 0.3, 0.7]
- I gate (identity): no laser activity
- CNOT (entanglement): control + target lasers fire together, trap shakes
"""

from typing import List, Tuple

from oqd_teaching_demo.digital import Circuit, UnaryGate, BinaryGate
from oqd_teaching_demo.program import Program

########################################################################################

__all__ = ["compile_circuit"]

########################################################################################

N_LASERS = 4

# Each gate type produces a sequence of intensity values for its target laser.
GATE_PATTERNS = {
"I": [],
"X": [1.0, 1.0],
"Z": [0.5, 0.5],
"H": [0.7, 0.3, 0.7],
}

########################################################################################


def compile_circuit(
circuit: Circuit, dt: float = 0.3
) -> Tuple[Program, List[Tuple[int, str]]]:
"""
Compile a Circuit into laser intensity sequences and trap commands.

Returns:
(Program, trap_commands) where trap_commands is a list of
(step_index, trap_mode) tuples for coordinating trap movement
during CNOT gates.
"""
all_intensities: List[List[float]] = []
trap_commands: List[Tuple[int, str]] = []

for instruction in circuit.instructions:
if isinstance(instruction, UnaryGate):
pattern = GATE_PATTERNS.get(instruction.gate, [])
if not pattern:
continue
target = instruction.target
if target >= N_LASERS:
continue
for intensity in pattern:
row = [0.0] * N_LASERS
row[target] = intensity
all_intensities.append(row)

elif isinstance(instruction, BinaryGate):
control = instruction.control
target = instruction.target
if control >= N_LASERS or target >= N_LASERS:
continue

# Record trap shake start for ion-ion interaction
trap_commands.append((len(all_intensities), "shake"))

# Step 1: Control qubit illuminated (identify the control)
row = [0.0] * N_LASERS
row[control] = 0.6
all_intensities.append(row)

# Step 2-3: Both qubits illuminated (interaction)
for _ in range(2):
row = [0.0] * N_LASERS
row[control] = 0.6
row[target] = 1.0
all_intensities.append(row)

# Step 4: Target qubit only (gate applied)
row = [0.0] * N_LASERS
row[target] = 1.0
all_intensities.append(row)

# Record trap stop after CNOT completes
trap_commands.append((len(all_intensities), "stop"))

if not all_intensities:
all_intensities = [[0.0] * N_LASERS]

return Program(red_lasers_intensity=all_intensities, dt=dt), trap_commands
129 changes: 129 additions & 0 deletions src/oqd_teaching_demo/digital_simulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright 2024-2025 Open Quantum Design

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Self-contained quantum state simulator for digital circuits.

Provides functions to simulate quantum circuits defined with the digital.py
gate models, compute measurement probabilities, and sample measurement outcomes.
The math follows the same kronecker product approach as emulator.py but without
the external Transformer dependency.
"""

import functools
from typing import Dict, List

import numpy as np

from oqd_teaching_demo.digital import Circuit, UnaryGate, BinaryGate

########################################################################################

__all__ = [
"simulate_circuit",
"get_probabilities",
"get_state_labels",
"sample_measurements",
]

########################################################################################

basis_map = {
"ket0": np.array([[1, 0]]).T,
"ket1": np.array([[0, 1]]).T,
}

gate_map = {
"I": np.array([[1, 0], [0, 1]], dtype=complex),
"H": np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2),
"X": np.array([[0, 1], [1, 0]], dtype=complex),
"Z": np.array([[1, 0], [0, -1]], dtype=complex),
}

########################################################################################


def _build_unary_operator(gate_name: str, target: int, n_qubits: int) -> np.ndarray:
"""Build full-system operator for a single-qubit gate via kronecker product."""
return functools.reduce(
np.kron,
[
gate_map[gate_name] if i == target else gate_map["I"]
for i in range(n_qubits)
],
)


def _build_cnot_operator(control: int, target: int, n_qubits: int) -> np.ndarray:
"""Build full-system CNOT operator: |0><0| x I + |1><1| x X."""
proj0 = basis_map["ket0"] @ basis_map["ket0"].T
proj1 = basis_map["ket1"] @ basis_map["ket1"].T

term0 = functools.reduce(
np.kron,
[proj0 if i == control else gate_map["I"] for i in range(n_qubits)],
)
term1 = functools.reduce(
np.kron,
[
(
proj1
if i == control
else (gate_map["X"] if i == target else gate_map["I"])
)
for i in range(n_qubits)
],
)
return term0 + term1


def simulate_circuit(circuit: Circuit) -> np.ndarray:
"""Simulate a quantum circuit and return the final state vector."""
n = circuit.N
initial_state = functools.reduce(
np.kron, [basis_map["ket0"] for _ in range(n)]
)

state = initial_state.astype(complex)
for instruction in circuit.instructions:
if isinstance(instruction, UnaryGate):
op = _build_unary_operator(instruction.gate, instruction.target, n)
elif isinstance(instruction, BinaryGate):
op = _build_cnot_operator(instruction.control, instruction.target, n)
else:
continue
state = op @ state

return state


def get_probabilities(state: np.ndarray) -> np.ndarray:
"""Compute measurement probabilities |amplitude|^2 for each basis state."""
return np.abs(state.flatten()) ** 2


def get_state_labels(n_qubits: int) -> List[str]:
"""Generate basis state labels like |0000>, |0001>, etc."""
return [f"|{format(i, f'0{n_qubits}b')}>" for i in range(2**n_qubits)]


def sample_measurements(probabilities: np.ndarray, n_shots: int = 100) -> Dict[str, int]:
"""Simulate n_shots measurements and return a histogram of outcome counts."""
n_qubits = int(np.log2(len(probabilities)))
labels = get_state_labels(n_qubits)
indices = np.random.choice(len(probabilities), size=n_shots, p=probabilities)
counts = {label: 0 for label in labels}
for idx in indices:
counts[labels[idx]] += 1
return counts
Loading