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%

Next Steps