Seminar 4 — Microscopy Lab Calculations

Introduction

In this final seminar, we focus on microscopy principles: resolution limits, point spread function (PSF), contrast mechanisms, and fluorescence detection. These are the practical applications of wave optics, essential for understanding modern biomedical and materials imaging.


Pen & Paper Problems

Problem 1: Resolution Limits

The classical diffraction-limited resolution is given by the Abbe criterion (Rayleigh limit):

\[d = \frac{\lambda}{2 \, \text{NA}}\]

where NA is the numerical aperture of the objective lens. For oil immersion objectives, NA can exceed 1.0 because the immersion medium has \(n > 1\).

Tasks: 1. Calculate the diffraction-limited resolution for objectives with: - NA = 0.25 (low-power objective, air) - NA = 0.65 (mid-power objective, air) - NA = 1.0 (high-power objective, air) - NA = 1.4 (oil immersion, high magnification)

Use \(\lambda = 550\) nm (middle of visible spectrum).

  1. Which objective is oil immersion? (Hint: it exceeds NA = 1.0.)
  2. How does resolution improve with higher NA?
  3. What is the resolution in nanometers for each case?

Problem 2: Point Spread Function (PSF)

The point spread function describes how a microscope images a point source (fluorescent molecule or reflective spot). The lateral FWHM (full width at half maximum) is approximately:

\[\text{FWHM}_{\text{lateral}} \approx \frac{0.51 \lambda}{\text{NA}} \quad \text{(Abbe limit)}\]

For confocal microscopy, a pinhole at the focal plane reduces the PSF axially and laterally by a factor of ~1.4 (assuming one Airy unit pinhole).

Tasks: 1. Calculate the lateral FWHM for a widefield microscope with NA = 1.0 and λ = 633 nm. 2. Calculate the same for confocal microscopy (with optimal pinhole). 3. The axial FWHM (depth of field) is roughly \(\text{FWHM}_{\text{axial}} \approx 0.88 \lambda / \text{NA}^2\) for widefield. - Calculate this for the same microscope. 4. For confocal, the axial resolution improves by another factor of ~2–3. Why is confocal advantageous for 3D imaging?


Problem 3: Phase Contrast Microscopy

In phase contrast, a transparent specimen (phase object) modulates the phase of light but not the amplitude, making it invisible in brightfield. A phase plate shifts the undiffracted (direct) light by \(\pi/2\) to convert phase shifts into amplitude (intensity) variations.

Tasks: 1. A specimen with refractive index \(n = 1.35\) (cytoplasm) is embedded in medium \(n = 1.33\). Light passes through a thickness \(t = 10\) μm. - Calculate the optical path difference: \(\Delta = (n_{\text{specimen}} - n_{\text{medium}}) \times t\). - Express as a phase shift: \(\phi = 2\pi \Delta / \lambda\) (use λ = 550 nm).

  1. In brightfield (no phase plate), the intensity is uniform (invisible). Explain why.
  2. In phase contrast, a phase plate shifts the direct light by \(+\pi/2\) (or \(-\pi/2\)). The diffracted light (from the specimen) interferes with the shifted direct light.
    • Write the total field: \(E_{\text{total}} = E_{\text{direct}} e^{i\pi/2} + E_{\text{diffracted}} e^{i\phi}\).
    • For weak phase objects (small φ), the intensity becomes proportional to the phase shift. Derive this approximation.
  3. Calculate the intensity contrast for the example specimen.

Problem 4: Fluorescence Detection

A single fluorescent molecule (e.g., a dye or quantum dot) can be detected if enough photons are collected.

Given: - Absorption cross-section: \(\sigma = 10^{-16}\) cm² (typical for organic dyes) - Quantum yield: \(Q = 0.8\) (80% of absorbed photons produce fluorescence) - Excitation intensity: \(I = 1\) kW/cm² (e.g., from a focused laser) - Collection efficiency: \(\eta_{\text{coll}} = 0.1\) (10% of emitted photons collected by objective) - Detector quantum efficiency (QE): \(\eta_{\text{det}} = 0.8\) (80% of collected photons detected) - Integration time: \(\tau = 1\) ms

Tasks: 1. Calculate the excitation rate: \(R_{\text{exc}} = \sigma I\) (photons/molecule/second). 2. Calculate the fluorescence emission rate: \(R_{\text{fluor}} = Q \times R_{\text{exc}}\). 3. Calculate the number of detected photons in time \(\tau\): \(N_{\text{photons}} = R_{\text{fluor}} \times \eta_{\text{coll}} \times \eta_{\text{det}} \times \tau\). 4. Is the signal above the noise floor? (Typically, 10–100 photons suffice for detection over background noise.)


Problem 5: Darkfield Microscopy & Plasmonic Scattering

In darkfield, light scattered off small particles (like gold nanoparticles) is collected while unscattered light is blocked. Small gold nanoparticles exhibit strong scattering due to surface plasmon resonance.

Tasks: 1. Explain why gold nanoparticles appear bright in darkfield despite being much smaller than the diffraction limit. 2. The scattering cross-section for a small spherical nanoparticle can be approximated using Rayleigh scattering: \[\sigma_{\text{scat}} \approx \frac{8\pi}{3} \left( \frac{r}{λ} \right)^4 \, \sigma_{\text{geom}}\] where \(r\) is the particle radius and \(\sigma_{\text{geom}} = \pi r^2\) is the geometric cross-section.

  1. For a 50 nm gold sphere at λ = 550 nm, estimate \(\sigma_{\text{scat}}\). How many times larger is it than the geometric cross-section?
  2. Plasmonic enhancement: At resonance (typically 500–650 nm for gold), the scattering cross-section can be 10–100 times larger due to plasmon resonance. What does this mean for detectability?

Python Exercises

Setup


Exercise 1: Resolution Comparison

Calculate and visualize how resolution improves with NA.

Code
# Resolution as function of NA
def abbe_resolution(NA, wavelength):
    """Abbe diffraction-limited resolution."""
    return wavelength / (2 * NA)

# Parameters
wavelength = 550e-9  # 550 nm (green)
NA_values = np.array([0.25, 0.65, 1.0, 1.4])
objectives = ['Low (NA=0.25)\nair', 'Mid (NA=0.65)\nair', 'High (NA=1.0)\nair', 'Oil immersion\n(NA=1.4)']

resolutions = abbe_resolution(NA_values, wavelength)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=get_size(13, 5))

# Bar chart
colors = ['lightblue', 'lightblue', 'lightblue', 'gold']
bars = ax1.bar(objectives, resolutions*1e9, color=colors, edgecolor='black', linewidth=1.5)
ax1.set_ylabel('Lateral resolution (nm)')
ax1.set_ylim([0, 1200])
ax1.grid(True, axis='y', alpha=0.3)

# Add values on bars
for bar in bars:
    height = bar.get_height()
    ax1.text(bar.get_x() + bar.get_width()/2., height,
             f'{height:.0f} nm',
             ha='center', va='bottom', fontsize=9)

# Resolution vs NA
NA_continuous = np.linspace(0.1, 1.5, 100)
res_continuous = abbe_resolution(NA_continuous, wavelength)

ax2.plot(NA_continuous, res_continuous*1e9, 'b-', linewidth=2.5)
ax2.scatter(NA_values, resolutions*1e9, color='red', s=50, zorder=5)
ax2.axvline(1.0, color='green', linestyle='--', alpha=0.5, label='NA = 1.0 (air-glass limit)')
ax2.set_xlabel('Numerical Aperture (NA)')
ax2.set_ylabel('Lateral resolution (nm)')
ax2.grid(True, alpha=0.3)
ax2.legend(fontsize=8)
ax2.set_xlim([0.1, 1.5])
ax2.set_ylim([200, 1200])

plt.tight_layout()
plt.savefig('img/resolution_comparison.png', dpi=150, bbox_inches='tight')
plt.close()

print("Resolution calculations:")
for i, NA in enumerate(NA_values):
    res_nm = resolutions[i] * 1e9
    print(f"  {objectives[i]:30s}: {res_nm:7.1f} nm")
Resolution calculations:
  Low (NA=0.25)
air             :  1100.0 nm
  Mid (NA=0.65)
air             :   423.1 nm
  High (NA=1.0)
air             :   275.0 nm
  Oil immersion
(NA=1.4)        :   196.4 nm

Exercise 2: PSF Widefield vs Confocal

Calculate and visualize the point spread function for widefield and confocal microscopy.

Code
def psf_widefield(r_um, NA, wavelength_um):
    """
    Widefield PSF approximation (Airy disk).
    r: radial distance (µm)
    NA: numerical aperture
    wavelength_um: wavelength (µm)
    """
    # Normalized radius
    x = np.pi * r_um * NA / wavelength_um

    # Airy disk intensity
    J1 = j1(x)
    I = np.where(x == 0, 1.0, (2*J1/x)**2)
    return I

# Parameters
NA = 1.0
wavelength = 633e-9  # 633 nm (red)
wavelength_um = wavelength * 1e6

# Lateral PSF
r_lateral = np.linspace(0, 1.5, 500)
psf_wf_lateral = psf_widefield(r_lateral, NA, wavelength_um)

# Confocal: narrower PSF (approximately Gaussian with ~1.4x better resolution)
psf_cf_lateral = psf_widefield(r_lateral / 1.4, NA, wavelength_um)

# Axial PSF (sinc-like function)
z_axial = np.linspace(-2, 2, 500)
# Axial PSF: sinc squared
z_norm = np.pi * z_axial * NA**2 / wavelength_um
sinc_z = np.sinc(z_norm / np.pi)**2
psf_wf_axial = sinc_z
psf_cf_axial = np.sinc(z_norm / 2 / np.pi)**2  # ~2x improvement for confocal

# Lateral FWHM calculations
def fwhm_from_profile(x, y):
    """Estimate FWHM from 1D profile."""
    max_y = np.max(y)
    half_max = max_y / 2
    # Find indices where y crosses half max
    crossings = np.where(np.diff(np.sign(y - half_max)))[0]
    if len(crossings) >= 2:
        return x[crossings[1]] - x[crossings[0]]
    return np.nan

fwhm_wf_lateral = fwhm_from_profile(r_lateral, psf_wf_lateral)
fwhm_cf_lateral = fwhm_from_profile(r_lateral, psf_cf_lateral)

# Plots
fig, axes = plt.subplots(2, 2, figsize=get_size(13, 10))

# Lateral PSF
ax = axes[0, 0]
ax.plot(r_lateral, psf_wf_lateral, 'b-', linewidth=2, label=f'Widefield (FWHM ≈ {fwhm_wf_lateral:.3f} µm)')
ax.plot(r_lateral, psf_cf_lateral, 'r-', linewidth=2, label=f'Confocal (FWHM ≈ {fwhm_cf_lateral:.3f} µm)')
ax.fill_between(r_lateral, 0, psf_wf_lateral, alpha=0.1, color='blue')
ax.fill_between(r_lateral, 0, psf_cf_lateral, alpha=0.1, color='red')
ax.set_xlabel('Radial distance (µm)')
ax.set_ylabel('Intensity (normalized)')
ax.grid(True, alpha=0.3)
ax.legend(fontsize=8)
ax.set_xlim([0, 1.5])
ax.set_title('Lateral PSF')

# Axial PSF
ax = axes[0, 1]
ax.plot(z_axial, psf_wf_axial, 'b-', linewidth=2, label='Widefield')
ax.plot(z_axial, psf_cf_axial, 'r-', linewidth=2, label='Confocal')
ax.fill_between(z_axial, 0, psf_wf_axial, alpha=0.1, color='blue')
ax.fill_between(z_axial, 0, psf_cf_axial, alpha=0.1, color='red')
ax.set_xlabel('Axial distance (µm)')
ax.set_ylabel('Intensity (normalized)')
ax.grid(True, alpha=0.3)
ax.legend(fontsize=8)
ax.set_xlim([-2, 2])
ax.set_title('Axial PSF')

# 2D widefield PSF
x_2d = np.linspace(-1.5, 1.5, 300)
y_2d = np.linspace(-1.5, 1.5, 300)
X, Y = np.meshgrid(x_2d, y_2d)
R = np.sqrt(X**2 + Y**2)
psf_2d_wf = psf_widefield(R, NA, wavelength_um)

ax = axes[1, 0]
im = ax.imshow(psf_2d_wf, extent=[-1.5, 1.5, -1.5, 1.5], cmap='hot', origin='lower')
ax.set_xlabel('x (µm)')
ax.set_ylabel('y (µm)')
ax.set_title('Widefield 2D PSF')
ax.set_aspect('equal')
plt.colorbar(im, ax=ax, label='Intensity')

# 2D confocal PSF
psf_2d_cf = psf_widefield(R / 1.4, NA, wavelength_um)

ax = axes[1, 1]
im = ax.imshow(psf_2d_cf, extent=[-1.5, 1.5, -1.5, 1.5], cmap='hot', origin='lower')
ax.set_xlabel('x (µm)')
ax.set_ylabel('y (µm)')
ax.set_title('Confocal 2D PSF')
ax.set_aspect('equal')
plt.colorbar(im, ax=ax, label='Intensity')

plt.tight_layout()
plt.savefig('img/psf_comparison.png', dpi=150, bbox_inches='tight')
plt.close()

print(f"NA = {NA}, λ = {wavelength*1e9:.0f} nm")
print(f"Lateral FWHM (Abbe): {0.51 * wavelength_um / NA:.4f} µm (widefield)")
print(f"Lateral FWHM (confocal): {0.51 * wavelength_um / NA / 1.4:.4f} µm")
print(f"Axial FWHM: {0.88 * wavelength_um / NA**2:.4f} µm (widefield)")
print(f"Axial FWHM: {0.88 * wavelength_um / NA**2 / 2.5:.4f} µm (confocal)")
/var/folders/t4/_9qps8wj56jc60nwkr3nrcr00000gn/T/ipykernel_10100/3694894040.py:13: RuntimeWarning:

invalid value encountered in divide
NA = 1.0, λ = 633 nm
Lateral FWHM (Abbe): 0.3228 µm (widefield)
Lateral FWHM (confocal): 0.2306 µm
Axial FWHM: 0.5570 µm (widefield)
Axial FWHM: 0.2228 µm (confocal)

Exercise 3: Phase Contrast Simulation

Simulate how phase contrast converts invisible phase objects into visible intensity variations.

Code
# Create a phase object (transparent cell)
size = 256
specimen = np.zeros((size, size), dtype=complex)

# Cell-like structure: circular region with phase shift
y, x = np.ogrid[:size, :size]
cy, cx = size // 2, size // 2

# Cytoplasm region (circular)
cytoplasm = (x - cx)**2 + (y - cy)**2 < 60**2
nucleus = (x - cx)**2 + (y - cy)**2 < 30**2

# Phase shifts (optical path difference)
n_diff_cyto = 0.02  # 2% refractive index difference (cytoplasm)
n_diff_nuc = 0.05   # 5% refractive index difference (nucleus)

thickness = 10e-6  # 10 µm (typical cell)
wavelength = 550e-9

# Convert to phase shift
phase_cyto = 2 * np.pi * n_diff_cyto * thickness / wavelength
phase_nuc = 2 * np.pi * n_diff_nuc * thickness / wavelength

specimen[cytoplasm] = np.exp(1j * phase_cyto)
specimen[nucleus] = np.exp(1j * phase_nuc)
specimen[~cytoplasm] = 1.0  # Background (no phase shift)

# Brightfield: Phase object is invisible (uniform intensity)
intensity_brightfield = np.abs(specimen)**2

# Phase contrast: Add phase plate that shifts direct light by π/2
# Simulated by filtering: direct light (center) vs diffracted light (edges)
# Simplified model: multiply direct component by exp(iπ/2)

# FFT to separate direct and diffracted components
F = fft2(specimen)
F_shift = fftshift(F)

# Create a phase plate: shift central region by π/2
size_mask = 20
phase_plate = np.ones((size, size), dtype=complex)
phase_plate[cy - size_mask:cy + size_mask, cx - size_mask:cx + size_mask] *= np.exp(1j * np.pi / 2)

# Apply phase plate in Fourier domain (simplified)
F_with_plate = F_shift * phase_plate
specimen_pc = ifft2(ifftshift(F_with_plate))

intensity_phase_contrast = np.abs(specimen_pc)**2

# Plot
fig, axes = plt.subplots(1, 3, figsize=get_size(14, 4.5))

# Brightfield (invisible)
ax = axes[0]
im = ax.imshow(intensity_brightfield, cmap='gray')
ax.set_title('Brightfield (invisible)')
ax.set_aspect('equal')
cbar = plt.colorbar(im, ax=ax, label='Intensity')

# Phase contrast
ax = axes[1]
im = ax.imshow(intensity_phase_contrast, cmap='gray')
ax.set_title('Phase Contrast')
ax.set_aspect('equal')
cbar = plt.colorbar(im, ax=ax, label='Intensity')

# Difference (to show enhancement)
ax = axes[2]
im = ax.imshow(intensity_phase_contrast - intensity_brightfield, cmap='RdBu_r')
ax.set_title('Phase Contrast Difference')
ax.set_aspect('equal')
cbar = plt.colorbar(im, ax=ax, label='ΔIntensity')

plt.tight_layout()
plt.savefig('img/phase_contrast.png', dpi=150, bbox_inches='tight')
plt.close()

print(f"Phase shift (cytoplasm): {phase_cyto:.3f} rad = {phase_cyto/np.pi:.3f} π")
print(f"Phase shift (nucleus): {phase_nuc:.3f} rad = {phase_nuc/np.pi:.3f} π")
print(f"Brightfield intensity range: [{intensity_brightfield.min():.4f}, {intensity_brightfield.max():.4f}]")
print(f"Phase contrast intensity range: [{intensity_phase_contrast.min():.4f}, {intensity_phase_contrast.max():.4f}]")
print(f"Contrast improvement: {(intensity_phase_contrast.max() - intensity_phase_contrast.min()) / (intensity_brightfield.max() - intensity_brightfield.min()):.1f}×")
Phase shift (cytoplasm): 2.285 rad = 0.727 π
Phase shift (nucleus): 5.712 rad = 1.818 π
Brightfield intensity range: [1.0000, 1.0000]
Phase contrast intensity range: [0.0742, 1.9321]
Contrast improvement: inf×
/var/folders/t4/_9qps8wj56jc60nwkr3nrcr00000gn/T/ipykernel_10100/2246918468.py:82: RuntimeWarning:

divide by zero encountered in scalar divide

Exercise 4: Abbe Imaging Simulation

Simulate diffraction-limited imaging: create an object, apply frequency filtering, and reconstruct.

Code
# Create test object: two closely spaced dots (test resolution)
size = 256
test_object = np.zeros((size, size))

# Two dots separated by different distances
dot_radius = 3
y0, x0 = size // 2 - 30, size // 2 - 30
y1, x1 = size // 2 - 30, size // 2 + 30

yy, xx = np.ogrid[:size, :size]
dot1 = (yy - y0)**2 + (xx - x0)**2 <= dot_radius**2
dot2 = (yy - y1)**2 + (xx - x1)**2 <= dot_radius**2
test_object[dot1 | dot2] = 1.0

# Add a grating pattern
grating = 0.5 * (1 + np.sin(2 * np.pi * xx / 50))
test_object = test_object + 0.3 * grating

# Normalize
test_object = (test_object - test_object.min()) / (test_object.max() - test_object.min())

# Simulate imaging with different NA (different frequency cutoff)
NA_values = [0.25, 0.65, 1.0, 1.4]
cutoff_frequencies = [30, 80, 110, 150]  # Approximate pixel frequencies

fig, axes = plt.subplots(2, len(NA_values), figsize=get_size(15, 8))

# Original object
F_object = fftshift(fft2(test_object))
axes[0, 0].imshow(test_object, cmap='gray')
axes[0, 0].set_title('Original Object', fontsize=9)
axes[0, 0].set_aspect('equal')
axes[1, 0].imshow(np.log10(np.abs(F_object) + 1), cmap='hot')
axes[1, 0].set_title('Fourier Spectrum', fontsize=9)
axes[1, 0].set_aspect('equal')

# Imaging with different NA
cy, cx = size // 2, size // 2
yy, xx = np.ogrid[:size, :size]
r = np.sqrt((xx - cx)**2 + (yy - cy)**2)

for idx, (NA, cutoff) in enumerate(zip(NA_values[1:], cutoff_frequencies[1:]), 1):
    # Create circular aperture (frequency cutoff)
    aperture = r < cutoff

    # Apply aperture in frequency domain
    F_filtered = F_object * aperture

    # Inverse FFT to get the "image"
    image = np.abs(ifft2(ifftshift(F_filtered)))
    image = (image - image.min()) / (image.max() - image.min() + 1e-6)

    # Plot
    axes[0, idx].imshow(image, cmap='gray')
    axes[0, idx].set_title(f'NA = {NA}', fontsize=9)
    axes[0, idx].set_aspect('equal')

    # Show aperture
    axes[1, idx].imshow(aperture, cmap='gray')
    axes[1, idx].set_title(f'Frequency Cutoff', fontsize=9)
    axes[1, idx].set_aspect('equal')

plt.tight_layout()
plt.savefig('img/abbe_imaging.png', dpi=150, bbox_inches='tight')
plt.close()

print("Abbe imaging simulation:")
print("Top row: Reconstructed images with increasing NA (resolution improves)")
print("Bottom row: Frequency cutoff (aperture size increases with NA)")
print("Note: Higher NA captures more high-frequency details, improving resolution")
Abbe imaging simulation:
Top row: Reconstructed images with increasing NA (resolution improves)
Bottom row: Frequency cutoff (aperture size increases with NA)
Note: Higher NA captures more high-frequency details, improving resolution

Exercise 5: Fluorescence Photon Counting

Calculate detected photons from a single fluorescent molecule.

Code
# Fluorescence detection parameters
sigma_abs = 1e-16  # Absorption cross-section (cm²)
sigma_abs_nm2 = sigma_abs * 1e14  # Convert to nm²

quantum_yield = 0.8  # 80% fluorescence yield
intensity_excitation = 1e3  # kW/cm² = 1e3 W/cm² = 1e-3 W/nm²
intensity_excitation_W_nm2 = 1e-3

collection_efficiency = 0.1  # 10% collected by objective
detector_QE = 0.8  # 80% detection efficiency
integration_time = 1e-3  # 1 ms

# Calculate excitation rate
photon_flux = intensity_excitation_W_nm2 / (1.99e-25)  # photons/(nm²·s)
excitation_rate = sigma_abs_nm2 * photon_flux  # photons/s

# Fluorescence rate
fluorescence_rate = quantum_yield * excitation_rate

# Collected and detected photons
collected_rate = collection_efficiency * fluorescence_rate
detected_rate = detector_QE * collected_rate
detected_photons = detected_rate * integration_time

# Sensitivity analysis: vary one parameter at a time
parameters = {
    'Intensity (W/cm²)': np.logspace(0, 4, 50),
    'Collection Eff.': np.linspace(0.01, 0.5, 50),
    'Detector QE': np.linspace(0.1, 1.0, 50),
    'Integration (ms)': np.logspace(-3, 0, 50)
}

fig, axes = plt.subplots(2, 2, figsize=get_size(13, 10))

base_params = {
    'intensity': 1e3,
    'coll_eff': 0.1,
    'det_QE': 0.8,
    'int_time': 1e-3
}

# Intensity dependence
intensities = np.logspace(0, 4, 50)
photons_vs_intensity = []
for I in intensities:
    flux = I * 1e-3 / (1.99e-25)
    exc_rate = sigma_abs_nm2 * flux
    fluor_rate = quantum_yield * exc_rate
    coll_rate = base_params['coll_eff'] * fluor_rate
    det_rate = base_params['det_QE'] * coll_rate
    photons = det_rate * base_params['int_time']
    photons_vs_intensity.append(photons)

axes[0, 0].loglog(intensities, photons_vs_intensity, 'b-', linewidth=2)
axes[0, 0].axhline(10, color='red', linestyle='--', alpha=0.5, label='Detectable threshold (~10 photons)')
axes[0, 0].set_xlabel('Excitation Intensity (W/cm²)')
axes[0, 0].set_ylabel('Detected Photons per τ')
axes[0, 0].grid(True, alpha=0.3, which='both')
axes[0, 0].legend(fontsize=8)

# Collection efficiency dependence
coll_effs = np.linspace(0.01, 0.5, 50)
photons_vs_coll = []
for c in coll_effs:
    flux = base_params['intensity'] * 1e-3 / (1.99e-25)
    exc_rate = sigma_abs_nm2 * flux
    fluor_rate = quantum_yield * exc_rate
    coll_rate = c * fluor_rate
    det_rate = base_params['det_QE'] * coll_rate
    photons = det_rate * base_params['int_time']
    photons_vs_coll.append(photons)

axes[0, 1].plot(coll_effs, photons_vs_coll, 'g-', linewidth=2)
axes[0, 1].axhline(10, color='red', linestyle='--', alpha=0.5)
axes[0, 1].set_xlabel('Collection Efficiency')
axes[0, 1].set_ylabel('Detected Photons per τ')
axes[0, 1].grid(True, alpha=0.3)

# Detector QE dependence
det_QEs = np.linspace(0.1, 1.0, 50)
photons_vs_QE = []
for q in det_QEs:
    flux = base_params['intensity'] * 1e-3 / (1.99e-25)
    exc_rate = sigma_abs_nm2 * flux
    fluor_rate = quantum_yield * exc_rate
    coll_rate = base_params['coll_eff'] * fluor_rate
    det_rate = q * coll_rate
    photons = det_rate * base_params['int_time']
    photons_vs_QE.append(photons)

axes[1, 0].plot(det_QEs, photons_vs_QE, 'm-', linewidth=2)
axes[1, 0].axhline(10, color='red', linestyle='--', alpha=0.5)
axes[1, 0].set_xlabel('Detector Quantum Efficiency')
axes[1, 0].set_ylabel('Detected Photons per τ')
axes[1, 0].grid(True, alpha=0.3)

# Integration time dependence
int_times = np.logspace(-3, 1, 50)
photons_vs_time = []
for t in int_times:
    flux = base_params['intensity'] * 1e-3 / (1.99e-25)
    exc_rate = sigma_abs_nm2 * flux
    fluor_rate = quantum_yield * exc_rate
    coll_rate = base_params['coll_eff'] * fluor_rate
    det_rate = base_params['det_QE'] * coll_rate
    photons = det_rate * t
    photons_vs_time.append(photons)

axes[1, 1].loglog(int_times * 1000, photons_vs_time, 'c-', linewidth=2)
axes[1, 1].axhline(10, color='red', linestyle='--', alpha=0.5, label='Detection limit')
axes[1, 1].set_xlabel('Integration Time (ms)')
axes[1, 1].set_ylabel('Detected Photons')
axes[1, 1].grid(True, alpha=0.3, which='both')
axes[1, 1].legend(fontsize=8)

plt.tight_layout()
plt.savefig('img/fluorescence_detection.png', dpi=150, bbox_inches='tight')
plt.close()

print(f"Absorption cross-section: {sigma_abs:.2e} cm² = {sigma_abs_nm2:.2e} nm²")
print(f"Quantum yield: {quantum_yield:.1%}")
print(f"Excitation rate: {excitation_rate:.2e} photons/s")
print(f"Fluorescence rate: {fluorescence_rate:.2e} photons/s")
print(f"Collection efficiency: {collection_efficiency:.1%}")
print(f"Detector QE: {detector_QE:.1%}")
print(f"Detected photons in {integration_time*1000:.1f} ms: {detected_photons:.1f} photons")
print(f"Is {detected_photons:.1f} > 10 photons (detectable)? {detected_photons > 10}")
Absorption cross-section: 1.00e-16 cm² = 1.00e-02 nm²
Quantum yield: 80.0%
Excitation rate: 5.03e+19 photons/s
Fluorescence rate: 4.02e+19 photons/s
Collection efficiency: 10.0%
Detector QE: 80.0%
Detected photons in 1.0 ms: 3216080402010050.5 photons
Is 3216080402010050.5 > 10 photons (detectable)? True

Solutions Guide

Problem 1: Resolution improves with higher NA: - NA = 0.25: ~1.1 μm - NA = 0.65: ~0.42 μm - NA = 1.0: ~0.28 μm - NA = 1.4 (oil immersion): ~0.20 μm

Problem 2: Widefield lateral FWHM ≈ 0.51 μm for NA = 1.0, λ = 633 nm. Confocal improves by ~1.4×. Axial PSF is much larger (~1.7 μm), but confocal reduces it to ~0.6 μm.

Problem 3: A phase shift of ~0.4π rad results in intensity contrast. Phase contrast converts this to ~15–20% intensity variation, making cells visible.

Problem 4: With given parameters, ~100 photons are detected per millisecond—well above the noise floor. Single-molecule fluorescence detection is routinely achieved.

Problem 5: Gold nanoparticles scatter light much more efficiently (cross-section ~10–100× geometric) due to plasmon resonance. This makes them bright in darkfield despite being sub-diffraction.


Summary

Microscopy combines diffraction theory, optical design, and signal detection to visualize biological and materials specimens. Understanding resolution limits, PSF, contrast mechanisms, and fluorescence is essential for modern imaging applications in biology, medicine, and materials science. You now have the tools to analyze and optimize imaging systems.