Photonic Computing Tutorials
Step-by-step guides from basics to advanced implementations
Beginner
~15 minutes
Tutorial 1: Ring Resonator Basics
Learn the fundamentals of microring resonators and how they implement photonic weights.
Step 1: Create a Ring Resonator
from photonic import RingResonator
import numpy as np
import matplotlib.pyplot as plt
# Create a typical silicon microring
ring = RingResonator(
radius=10e-6, # 10 μm radius
coupling_gap=200e-9, # 200 nm gap
waveguide_width=500e-9,
loss_db_cm=2.0,
n_eff=2.4
)
print(f"FSR: {ring.get_fsr()*1e9:.2f} nm")
print(f"Q Factor: {ring.get_quality_factor():.0f}")
Step 2: Plot the Transmission Spectrum
# Wavelength sweep around 1550 nm
wavelengths = np.linspace(1545e-9, 1555e-9, 1000)
transmission = [ring.transmission(wl) for wl in wavelengths]
plt.figure(figsize=(10, 4))
plt.plot(wavelengths*1e9, 10*np.log10(transmission))
plt.xlabel('Wavelength (nm)')
plt.ylabel('Transmission (dB)')
plt.title('Ring Resonator Spectrum')
plt.grid(True, alpha=0.3)
plt.show()
Step 3: Tune the Resonance
# Tune resonance by 0.5 nm (thermal tuning)
heater_power = ring.tune_resonance(0.5e-9)
print(f"Heater power required: {heater_power:.1f} mW")
# Now check transmission at 1550 nm
T_original = ring.transmission(1550e-9)
T_tuned = ring.transmission(1550e-9) # After tuning
print(f"Weight change: {np.sqrt(T_original):.3f} → {np.sqrt(T_tuned):.3f}")
Key Takeaways:
- • Ring resonators act as wavelength-selective filters
- • The transmission at a given wavelength implements the weight value
- • Thermal tuning shifts the resonance to change the weight
Intermediate
~30 minutes
Tutorial 2: Your First Photonic MVM
Build a matrix-vector multiplication unit and understand quantization effects.
Step 1: Create the MVM Unit
from photonic import PhotonicMVM
import numpy as np
# Create an 8×8 MVM unit with 4-bit weights
mvm = PhotonicMVM(
size=(8, 8),
precision=4, # 16 weight levels
wavelengths=8, # One wavelength per column
clock_rate=10e9
)
print(f"Peak throughput: {mvm.get_throughput():.2f} TOPS")
print(f"Energy/MAC: {mvm.get_energy_per_mac():.1f} fJ")
Step 2: Load Weights and Run Inference
# Random weight matrix (will be quantized to 4-bit)
W_float = np.random.randn(8, 8) * 0.5
mvm.load_weights(W_float)
# Get quantized weights for comparison
W_quantized = mvm.get_weights()
# Run forward pass
x = np.array([1, 0.5, -0.3, 0.8, -0.2, 0.6, -0.7, 0.4])
y_photonic = mvm.forward(x)
# Compare with ideal floating-point result
y_ideal = W_float @ x
y_quantized = W_quantized @ x
print(f"Ideal output: {y_ideal[:3]}")
print(f"Quantized output: {y_quantized[:3]}")
print(f"Photonic output: {y_photonic[:3]}")
Step 3: Analyze Quantization Error
# Compare different precisions
precisions = [2, 4, 6, 8]
errors = []
for p in precisions:
mvm_test = PhotonicMVM(size=(8, 8), precision=p, wavelengths=8)
mvm_test.load_weights(W_float)
# Run many random inputs
total_mse = 0
for _ in range(100):
x_test = np.random.randn(8)
y_test = mvm_test.forward(x_test)
y_ref = W_float @ x_test
total_mse += np.mean((y_test - y_ref)**2)
errors.append(total_mse / 100)
print(f"{p}-bit precision: MSE = {errors[-1]:.4f}")
# Plot precision vs error
plt.semilogy(precisions, errors, 'o-', color='purple')
plt.xlabel('Weight Precision (bits)')
plt.ylabel('Mean Squared Error')
plt.grid(True, alpha=0.3)
Key Takeaways:
- • 4-bit weights are often sufficient for inference tasks
- • Quantization error decreases exponentially with precision
- • WDM enables parallel processing of all input elements
Intermediate
~25 minutes
Tutorial 3: Non-Volatile Weights with PCM
Store neural network weights using phase-change materials for zero static power.
Step 1: Understand PCM Properties
from photonic import PCMWeight
# Create a GST-based weight cell
pcm = PCMWeight(
material='gst',
thickness=30e-9, # 30 nm film
wavelength=1550e-9,
levels=16 # 4-bit precision
)
# Check optical properties at different states
for level in [0, 5, 10, 15]:
pcm.set_state(level)
n = pcm.get_refractive_index()
T = pcm.get_transmission()
print(f"Level {level}: n = {n.real:.2f} + {n.imag:.3f}j, T = {T:.3f}")
Step 2: Program Multi-Level States
# Program intermediate states with partial crystallization
states = np.linspace(0, 15, 16)
transmissions = []
energies = []
for state in states:
energy = pcm.set_state(int(state))
transmissions.append(pcm.get_transmission())
energies.append(energy)
# Plot transfer curve
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(states, transmissions, 'o-', color='purple')
plt.xlabel('Programmed Level')
plt.ylabel('Optical Transmission')
plt.title('PCM Transfer Curve')
plt.subplot(1, 2, 2)
plt.bar(states, energies, color='cyan', alpha=0.7)
plt.xlabel('Programmed Level')
plt.ylabel('Programming Energy (nJ)')
plt.title('Energy per Write')
plt.tight_layout()
Step 3: Compare Materials
materials = ['gst', 'gsst', 'aist']
plt.figure(figsize=(8, 5))
for mat in materials:
pcm = PCMWeight(material=mat, thickness=30e-9, wavelength=1550e-9, levels=16)
trans = []
for level in range(16):
pcm.set_state(level)
trans.append(pcm.get_transmission())
plt.plot(range(16), trans, 'o-', label=mat.upper())
plt.xlabel('State Level')
plt.ylabel('Transmission')
plt.legend()
plt.title('Material Comparison')
plt.grid(True, alpha=0.3)
Key Takeaways:
- • GSST has lower loss than GST, better for low-loss applications
- • PCM enables non-volatile storage—no power needed to retain weights
- • Programming energy is ~1 nJ per cell, much lower than DRAM refresh
Advanced
~45 minutes
Tutorial 4: Noise-Aware Inference
Model realistic noise sources and understand their impact on neural network accuracy.
Step 1: Set Up Noise Model
from photonic import PhotonicMVM, NoiseModel
import numpy as np
# Create MVM with noise
mvm = PhotonicMVM(size=(32, 32), precision=4, wavelengths=8, clock_rate=10e9)
noise = NoiseModel(
optical_power=1e-3, # 1 mW input power
bandwidth=20e9, # 20 GHz detection BW
temperature=300, # Room temperature
responsivity=0.9, # Photodetector responsivity
rin_dbhz=-150 # Laser RIN
)
# Attach noise model
mvm.set_noise_model(noise)
# Check noise breakdown
print(f"Shot noise: {noise.shot_noise()*1e12:.2f} pA/√Hz")
print(f"Thermal noise: {noise.thermal_noise(50)*1e12:.2f} pA/√Hz")
print(f"RIN noise: {noise.rin_noise()*1e12:.2f} pA/√Hz")
print(f"Total SNR: {noise.get_snr():.1f} dB")
print(f"Effective bits: {noise.get_enob():.1f}")
Step 2: Monte Carlo Error Analysis
# Load a weight matrix
W = np.random.randn(32, 32) * 0.1
mvm.load_weights(W)
# Run many trials to see noise distribution
x = np.random.randn(32)
outputs = []
for _ in range(1000):
y = mvm.forward(x) # Each call has different noise
outputs.append(y)
outputs = np.array(outputs)
# Analyze output statistics
y_mean = outputs.mean(axis=0)
y_std = outputs.std(axis=0)
y_ideal = W @ x
print(f"Mean error: {np.mean(np.abs(y_mean - y_ideal)):.4f}")
print(f"Std deviation: {np.mean(y_std):.4f}")
Step 3: Power vs Accuracy Trade-off
# Sweep optical power and measure accuracy
powers_dbm = np.linspace(-20, 10, 20)
snrs = []
mses = []
for p_dbm in powers_dbm:
p_watts = 10**(p_dbm/10) * 1e-3
noise_test = NoiseModel(optical_power=p_watts, bandwidth=20e9)
mvm.set_noise_model(noise_test)
snrs.append(noise_test.get_snr())
# Measure MSE
mse = 0
for _ in range(100):
y = mvm.forward(x)
mse += np.mean((y - y_ideal)**2)
mses.append(mse / 100)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(powers_dbm, snrs, 'o-', color='purple')
plt.xlabel('Optical Power (dBm)')
plt.ylabel('SNR (dB)')
plt.subplot(1, 2, 2)
plt.semilogy(powers_dbm, mses, 'o-', color='cyan')
plt.xlabel('Optical Power (dBm)')
plt.ylabel('MSE')
plt.tight_layout()
Expert
~60 minutes
Tutorial 5: Full Neural Network on Photonics
Map a complete neural network to photonic hardware and run MNIST inference.
Step 1: Load Pre-trained Model
import torch
import torch.nn as nn
from photonic import PhotonicMVM, map_network_to_tiles
# Simple MNIST classifier
class MNISTNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
return self.fc3(x)
# Load pre-trained weights
model = MNISTNet()
model.load_state_dict(torch.load('mnist_model.pth'))
Step 2: Map to Photonic Tiles
# Map each layer to photonic MVM tiles
tile_size = 32
precision = 4
layers = []
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
W = module.weight.detach().numpy()
# Create tiled MVM for this layer
tiles = map_network_to_tiles(W, tile_size, precision)
layers.append({
'name': name,
'tiles': tiles,
'num_tiles': len(tiles),
'input_dim': W.shape[1],
'output_dim': W.shape[0]
})
print(f"{name}: {W.shape} → {len(tiles)} tiles")
Step 3: Run Photonic Inference
def photonic_forward(x, layers):
"""Run inference through photonic network"""
for layer in layers:
# Tile the input
x_tiled = np.array_split(x, np.ceil(len(x) / tile_size))
# Process through each tile
outputs = []
for tile_row in layer['tiles']:
row_output = np.zeros(tile_size)
for i, tile in enumerate(tile_row):
if i < len(x_tiled):
row_output += tile.forward(x_tiled[i])
outputs.append(row_output)
# Combine tile outputs
x = np.concatenate(outputs)[:layer['output_dim']]
# Apply ReLU (electronic)
x = np.maximum(0, x)
return x
# Test on MNIST sample
test_image = mnist_test[0][0].flatten().numpy()
output = photonic_forward(test_image, layers)
prediction = np.argmax(output)
print(f"Predicted: {prediction}, Actual: {mnist_test[0][1]}")
Step 4: Benchmark Full Test Set
# Run on full test set
correct = 0
total = 0
for image, label in mnist_test:
x = image.flatten().numpy()
output = photonic_forward(x, layers)
pred = np.argmax(output)
if pred == label:
correct += 1
total += 1
accuracy = correct / total * 100
print(f"Photonic Accuracy: {accuracy:.2f}%")
# Compare with floating-point model
model.eval()
with torch.no_grad():
correct_fp = sum(
model(img.flatten().unsqueeze(0)).argmax() == lbl
for img, lbl in mnist_test
)
print(f"FP32 Accuracy: {correct_fp/total*100:.2f}%")
Performance Summary:
- • Total tiles: 104 (32×32 each)
- • Inference latency: ~1 μs
- • Energy per inference: ~50 μJ
- • Accuracy drop from FP32: typically <1%