Papillon Engineering Papillon Engineering
  • Home
  • Teaching
    • Teaching Statement
    • Lecture Notes
    • Lab Manuals Hub
    • Dual-Polarity Power Supply Wiring
  • Outreach
    • K-12 Outreach Hub
    • Linear Regression
    • API Data Challenge
    • Web Scraping
    • Resonance Defender Game
    • Impedance Breaker Game
    • RF Fox Hunt Lab
  • Circuit Analysis
    • Circuit Analysis Hub
    • Electrical Dashboard
    • Live Circuit Simulator
    • Reactance Calculator
    • Resonance Defender Game
    • Impedance Breaker Game
    • Circuit Problem Generator
  • Simulations
    • Simulations Hub
    • 1951 USAF Optical Resolution Analyzer
    • Live Circuit Simulator
    • Circuit Problem Generator
    • Reactance Calculator
    • Resonance Defender Game
    • Impedance Breaker Game
  • Projects

On this page…

  • 🚀 Mission Brief: The Gale Crater Anomaly
  • 🔬 The Physics of X-Ray Diffraction
  • 🎛️ Interactive Lab: Periodic Table & Noise Filter

Mars Rover XRD: Fingerprinting the Red Planet

K-12 Materials Science: Bragg’s Law, Crystal Lattices, and Alloy Composition

Affiliate Disclosure: Some links below are affiliate links. If you purchase hardware or data tools through these links, this site earns a small commission at no additional cost to you, helping fund our K-12 STEM outreach programs.


🚀 Mission Brief: The Gale Crater Anomaly

Welcome to the Astro-Materials Analysis Lab. NASA’s Curiosity rover (equipped with the CheMin X-Ray Diffraction instrument) has just drilled into an unusual metallic vein in Gale Crater on Mars. Before mission control authorizes a sample return, our engineering team must identify what the rock is made of without touching it!

Using X-Ray Diffraction (XRD), we shoot a beam of X-rays at the powdered rock sample. As the X-rays bounce off the internal crystal planes of the atoms, they create a unique interference pattern—an atomic “fingerprint” called a diffractogram.

Data Attribution: Core diffraction analysis workflows are inspired by synchrotron and planetary exploration curricula from Brookhaven National Laboratory (BNL) and NASA’s Mars Science Laboratory.

d Incident X-Rays (λ) Diffracted Signal (2θ) θ Bragg's Law nλ = 2d sin(θ) Cu Kα (λ = 0.15406 nm)

🔬 The Physics of X-Ray Diffraction

When an X-ray beam with a known wavelength (\lambda) strikes a crystal lattice at an angle (\theta), the waves bounce off parallel layers of atoms separated by a distance (d). Constructive interference (a sharp spike in intensity on our detector) only happens when the extra distance traveled by the deeper wave equals a whole number (n) of wavelengths:

n\lambda = 2d \sin(\theta)

Because our X-ray detector rotates around the sample, we measure the total deflection angle, known as 2\theta (Two-Theta). By finding the exact 2\theta angles where high-intensity peaks occur, we can rearrange Bragg’s Law to calculate the exact atomic spacing (d) inside the mystery rock:

d = \frac{\lambda}{2 \sin(\theta)}



🎛️ Interactive Lab: Periodic Table & Noise Filter

Before running our Python scripts, use this live mission simulator! Select a pure reference element from the Periodic Table or load the Mystery Mars Alloy. Use the slider to subtract the background “Martian dust scattering” (noise floor) and isolate the true crystalline peaks!

⚡ CheMin Rover Live Diffractogram Simulator



  • 👨‍💻 Student Lab Guide
  • 👨‍🏫 Instructor Guide & Answer Key

Step 1: Generating & Loading the Mars Rover Dataset

In the field (or in Google Colab), our spectrometer exports a CSV file with two columns: 2-THETA (from 3.00^\circ to 51.95^\circ in 0.05^\circ steps, giving 980 rows) and INTENSITY (photon counts). Below, we generate the exact Martian diffractogram dataset in Python and inspect the DataFrame!

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Generate the 980-row Mars Rover XRD Dataset (3.00 to 51.95 degrees, step 0.05)
np.random.seed(42)
two_theta = np.round(np.arange(3.00, 52.00, 0.05), 2)

# Simulate low-angle dust scattering + crystalline mineral peaks (Max 10,000 at 32.35 deg)
background = 3400 * np.exp(-(two_theta - 3.0) / 14.0) + 1400 + np.random.normal(0, 140, len(two_theta))
peak_main = 7800 * np.exp(-((two_theta - 32.35) / 0.35)**2)
peak_sec1 = 1800 * np.exp(-((two_theta - 26.60) / 0.30)**2)
peak_sec2 = 1250 * np.exp(-((two_theta - 42.45) / 0.40)**2)

intensity = np.clip(background + peak_main + peak_sec1 + peak_sec2, 1200, 10000).astype(int)
intensity[np.where(two_theta == 32.35)[0][0]] = 10000 # Calibrate max peak

df = pd.DataFrame({"2-THETA": two_theta, "INTENSITY": intensity})
print("Dataset Columns:", df.columns.tolist())
print(df.head())
Dataset Columns: ['2-THETA', 'INTENSITY']
   2-THETA  INTENSITY
0     3.00       4869
1     3.05       4768
2     3.10       4866
3     3.15       4976
4     3.20       4718

Step 2: Plot 1 — Raw Detector Intensity Stream

Before mapping the angles, let’s plot the raw INTENSITY column by row index to verify our X-ray detector didn’t drop any data packets.

plt.figure(figsize=(8, 4))
df["INTENSITY"].plot(color="#0284c7", linewidth=1.5)
plt.title("Plot 1: Raw Detector Intensity Stream (Index vs. Counts)", fontweight="bold")
plt.xlabel("Row Index (0 to 979)")
plt.ylabel("Photon Intensity")
plt.tight_layout()
plt.show()

Step 3: Plot 2 — Standard X-Ray Diffractogram (2\theta vs. Intensity)

Now let’s map 2-THETA on the X-axis and INTENSITY on the Y-axis, and use Pandas .idxmax() to automatically locate the highest crystalline peak in the Martian rock!

# Find the exact row with the maximum X-ray intensity
max_idx = df["INTENSITY"].idxmax()
print("=== MAXIMUM DIFFRACTION PEAK DETECTED ===")
print(df.iloc[max_idx])

df.plot(x="2-THETA", y="INTENSITY", color="#ea580c", figsize=(8, 4), legend=False)
plt.title("Plot 2: Standard X-Ray Diffractogram (2-Theta vs. Intensity)", fontweight="bold")
plt.xlabel("2-Theta (Degrees)")
plt.ylabel("Intensity (Counts)")
plt.tight_layout()
plt.show()
=== MAXIMUM DIFFRACTION PEAK DETECTED ===
2-THETA         32.35
INTENSITY    10000.00
Name: 587, dtype: float64

Step 4: Plot 3 — High-Contrast Diagnostic Scan with Grid

In materials science publications, diffractograms are plotted in high-contrast black with a coordinate grid so researchers can read exact 2\theta peak positions and calculate atomic d-spacing using Bragg’s Law.

df.plot(x="2-THETA", y="INTENSITY", color="black", grid=True, figsize=(10, 4),
        title="Plot 3: High-Contrast Mars XRD Data with Diagnostic Grid",
        label="Intensity of Mars Rock")
plt.xlabel("2-Theta Angle (Degrees)")
plt.ylabel("Intensity (Counts)")
plt.axvline(x=32.35, color="red", linestyle="--", alpha=0.7, label="Primary Peak (32.35°)")
plt.legend()
plt.tight_layout()
plt.show()

Step 5: Plot 4 — Comparing Known Pure Elements (Si, Al, Au) & Bragg’s Law

How do we identify unknown peaks? We compare them against pure elements from the Periodic Table! Let’s use Bragg’s Law (\lambda = 0.15406\text{ nm} for a Copper K\alpha X-ray tube) to calculate the atomic plane spacing (d) and plot the fingerprints for Silicon (Si), Aluminum (Al), and Gold (Au).

wavelength = 0.15406 # Cu K-alpha X-ray wavelength in nanometers (nm)

# Reference primary (111) peaks in 2-Theta degrees
standards = {
    "Silicon (Si)": {"two_theta": 28.44, "color": "#0284c7"},
    "Gold (Au)": {"two_theta": 38.18, "color": "#eab308"},
    "Aluminum (Al)": {"two_theta": 38.47, "color": "#10b981"}
}

angles = np.linspace(20, 60, 800)
plt.figure(figsize=(10, 4.5))

for name, info in standards.items():
    theta_rad = np.radians(info["two_theta"] / 2.0)
    d_spacing = wavelength / (2.0 * np.sin(theta_rad))
    print(f"{name:15s} | 2-Theta: {info['two_theta']}° | Atomic d-spacing: {d_spacing:.4f} nm")
    
    # Simulate clean reference peak
    sim_curve = 8500 * np.exp(-((angles - info["two_theta"]) / 0.35)**2) + 200
    plt.plot(angles, sim_curve, label=f"{name} (d = {d_spacing:.3f} nm)", color=info["color"], linewidth=2)

plt.title("Plot 4: Reference Element Standards (Silicon, Gold, Aluminum)", fontweight="bold")
plt.xlabel("2-Theta (Degrees)")
plt.ylabel("Normalized Intensity")
plt.grid(True, linestyle="--", alpha=0.5)
plt.legend()
plt.tight_layout()
plt.show()
Silicon (Si)    | 2-Theta: 28.44° | Atomic d-spacing: 0.3136 nm
Gold (Au)       | 2-Theta: 38.18° | Atomic d-spacing: 0.2355 nm
Aluminum (Al)   | 2-Theta: 38.47° | Atomic d-spacing: 0.2338 nm

Step 6: Plot 5 — The Alloy Challenge: Weight Percentage & Ratios

Wait—the rover’s drill bit chipped! A metallic flake fell into the sample chamber, and we need to verify if it’s a piece of the rover’s Aluminum-Silicon (Al-Si) aerospace alloy strut.

In an alloy mixture, the weight percentage of each metal is proportional to its integrated peak intensity (Adjusted for Reference Intensity Ratio constants). Let’s analyze a dual-phase Al-Si diffractogram, calculate the Intensity Ratio, and determine the Weight Percentage (\text{wt}\%)!

# Simulate an Al-Si Alloy Diffractogram
alloy_angles = np.linspace(20, 55, 700)
si_peak_int = 2400 # Silicon (111) at 28.44 deg
al_peak_int = 7200 # Aluminum (111) at 38.47 deg

alloy_signal = (300 + 
                si_peak_int * np.exp(-((alloy_angles - 28.44) / 0.35)**2) + 
                al_peak_int * np.exp(-((alloy_angles - 38.47) / 0.35)**2) +
                3200 * np.exp(-((alloy_angles - 44.74) / 0.40)**2))

# Quantitative Composition Math
total_primary_intensity = al_peak_int + si_peak_int
al_wt_pct = (al_peak_int / total_primary_intensity) * 100
si_wt_pct = (si_peak_int / total_primary_intensity) * 100
ratio_al_si = al_peak_int / si_peak_int

print(f"=== ALLOY QUANTITATIVE AUDIT ===")
print(f"Aluminum Peak (38.47°): {al_peak_int} counts")
print(f"Silicon Peak  (28.44°): {si_peak_int} counts")
print(f"Intensity Ratio (Al:Si): {ratio_al_si:.1f} : 1")
print(f"Calculated Composition : {al_wt_pct:.1f} wt% Aluminum | {si_wt_pct:.1f} wt% Silicon")

# Plot 5: Side-by-Side Spectrum & Composition Pie/Bar Breakdown
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2), gridspec_kw={'width_ratios': [2.2, 1]})

ax1.plot(alloy_angles, alloy_signal, color="#1e293b", linewidth=2)
ax1.fill_between(alloy_angles, 300, alloy_signal, color="#38bdf8", alpha=0.2)
ax1.annotate(f"Si (111)\n{si_wt_pct:.0f}%", xy=(28.44, si_peak_int+300), xytext=(23, 5000),
             arrowprops=dict(arrowstyle="->", color="#0284c7", lw=1.5), fontweight="bold", color="#0284c7")
ax1.annotate(f"Al (111)\n{al_wt_pct:.0f}%", xy=(38.47, al_peak_int+300), xytext=(32, 8000),
             arrowprops=dict(arrowstyle="->", color="#10b981", lw=1.5), fontweight="bold", color="#10b981")
ax1.set_title("Plot 5A: Al-Si Alloy Diffraction Spectrum", fontweight="bold")
ax1.set_xlabel("2-Theta (Degrees)")
ax1.set_ylabel("Intensity (Counts)")
ax1.grid(True, linestyle="--", alpha=0.5)

ax2.bar(["Aluminum\n(Al)", "Silicon\n(Si)"], [al_wt_pct, si_wt_pct], color=["#10b981", "#0284c7"], edgecolor="black")
ax2.set_ylim(0, 100)
ax2.set_ylabel("Weight Percentage (wt%)")
ax2.set_title("Plot 5B: Alloy Weight %", fontweight="bold")
ax2.grid(axis="y", linestyle="--", alpha=0.5)

plt.tight_layout()
plt.show()
=== ALLOY QUANTITATIVE AUDIT ===
Aluminum Peak (38.47°): 7200 counts
Silicon Peak  (28.44°): 2400 counts
Intensity Ratio (Al:Si): 3.0 : 1
Calculated Composition : 75.0 wt% Aluminum | 25.0 wt% Silicon


Pedagogical Goals

  1. Connecting Wave Physics to Data Science: Students see how trigonometry (\sin\theta) and wave interference translate directly into tabular CSV arrays that can be queried with .idxmax() and .iloc[].
  2. Reading 2\theta vs. \theta: A classic student mistake in Bragg’s Law (n\lambda = 2d\sin\theta) is plugging the 2\theta graph value directly into \sin(\theta) without dividing the angle by 2 first! Emphasize that if the peak is at 2\theta = 32.35^\circ, then \theta = 16.175^\circ.

Answer Key for Student Challenges

  • Mystery Peak d-Spacing Calculation: At 2\theta = 32.35^\circ, \theta = 16.175^\circ. Using \lambda = 0.15406\text{ nm}: d = \frac{0.15406}{2 \times \sin(16.175^\circ)} = \frac{0.15406}{2 \times 0.27857} = 0.2765\text{ nm } (2.765\text{ \AA}) Mineral Match: This d-spacing matches calcium-rich plagioclase feldspar and augite, common volcanic basalt minerals in Gale Crater!

  • Interactive Python Widget Extension (For Jupyter/Colab): If running this lesson live inside Google Colab or JupyterLab, students can run the following ipywidgets snippet to create a native Python slider:

    import ipywidgets as widgets
    from IPython.display import display
    
    def filter_xrd(threshold=2000):
        filtered_df = df[df["INTENSITY"] >= threshold]
        plt.figure(figsize=(9, 3.5))
        plt.plot(filtered_df["2-THETA"], filtered_df["INTENSITY"], color="black")
        plt.title(f"Filtered XRD Data (Intensity >= {threshold})")
        plt.xlabel("2-Theta")
        plt.ylabel("Intensity")
        plt.grid(True)
        plt.show()
    
    widgets.interact(filter_xrd, threshold=widgets.IntSlider(min=1000, max=8000, step=250, value=2000));