Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/earth-science/radiometric_dating.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{report}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb}
5
\usepackage{graphicx}
6
\usepackage{siunitx}
7
\usepackage{booktabs}
8
\usepackage{xcolor}
9
\usepackage[makestderr]{pythontex}
10
11
\definecolor{parent}{RGB}{231, 76, 60}
12
\definecolor{daughter}{RGB}{46, 204, 113}
13
\definecolor{isochron}{RGB}{52, 152, 219}
14
15
\title{Radiometric Dating:\\
16
Isotope Geochronology and Age Determination}
17
\author{Department of Earth Science\\Technical Report ES-2024-001}
18
\date{\today}
19
20
\begin{document}
21
\maketitle
22
23
\begin{abstract}
24
This report presents a comprehensive analysis of radiometric dating methods. We examine radioactive decay kinetics, implement isochron dating for Rb-Sr and U-Pb systems, analyze concordia-discordia relationships, calculate closure temperatures, and demonstrate carbon-14 dating for recent samples. All computations use PythonTeX for reproducibility.
25
\end{abstract}
26
27
\tableofcontents
28
29
\chapter{Introduction}
30
31
Radiometric dating determines absolute ages of geological materials using radioactive decay:
32
\begin{equation}
33
N(t) = N_0 e^{-\lambda t}
34
\end{equation}
35
where $N_0$ is the initial number of parent atoms, $\lambda$ is the decay constant, and $t$ is time.
36
37
\section{Decay Constant and Half-Life}
38
The relationship between decay constant and half-life:
39
\begin{equation}
40
\lambda = \frac{\ln 2}{t_{1/2}}
41
\end{equation}
42
43
\section{Age Equation}
44
From the parent-daughter relationship:
45
\begin{equation}
46
D = D_0 + N(e^{\lambda t} - 1)
47
\end{equation}
48
49
\begin{pycode}
50
import numpy as np
51
import matplotlib.pyplot as plt
52
from scipy.optimize import curve_fit
53
from scipy.stats import linregress
54
plt.rc('text', usetex=True)
55
plt.rc('font', family='serif')
56
57
np.random.seed(42)
58
59
# Isotope systems database
60
isotope_systems = {
61
'U-238/Pb-206': {'half_life': 4.468e9, 'lambda': np.log(2)/4.468e9},
62
'U-235/Pb-207': {'half_life': 7.038e8, 'lambda': np.log(2)/7.038e8},
63
'Rb-87/Sr-87': {'half_life': 4.88e10, 'lambda': np.log(2)/4.88e10},
64
'K-40/Ar-40': {'half_life': 1.248e9, 'lambda': np.log(2)/1.248e9},
65
'Sm-147/Nd-143': {'half_life': 1.06e11, 'lambda': np.log(2)/1.06e11},
66
'C-14/N-14': {'half_life': 5730, 'lambda': np.log(2)/5730}
67
}
68
69
def decay_curve(t, half_life):
70
return np.exp(-np.log(2) * t / half_life)
71
72
def daughter_growth(t, half_life):
73
return 1 - np.exp(-np.log(2) * t / half_life)
74
75
def calculate_age(D_star, N, lambda_val):
76
return np.log(1 + D_star/N) / lambda_val
77
78
def isochron_age(slope, lambda_val):
79
return np.log(slope + 1) / lambda_val
80
\end{pycode}
81
82
\chapter{Radioactive Decay Kinetics}
83
84
\begin{pycode}
85
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
86
87
# Decay curves for different systems
88
ax = axes[0, 0]
89
systems = ['U-238/Pb-206', 'U-235/Pb-207', 'Rb-87/Sr-87', 'K-40/Ar-40']
90
colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12']
91
for system, color in zip(systems, colors):
92
t_max = 5 * isotope_systems[system]['half_life']
93
t = np.linspace(0, t_max, 1000)
94
N_N0 = decay_curve(t, isotope_systems[system]['half_life'])
95
ax.plot(t/1e9, N_N0, label=system.split('/')[0], color=color, linewidth=2)
96
97
ax.set_xlabel('Time (Ga)')
98
ax.set_ylabel('$N/N_0$')
99
ax.set_title('Parent Isotope Decay')
100
ax.legend(fontsize=8)
101
ax.grid(True, alpha=0.3)
102
ax.set_xlim(0, 20)
103
104
# Daughter growth
105
ax = axes[0, 1]
106
for system, color in zip(systems, colors):
107
t_max = 5 * isotope_systems[system]['half_life']
108
t = np.linspace(0, t_max, 1000)
109
D_D_inf = daughter_growth(t, isotope_systems[system]['half_life'])
110
ax.plot(t/1e9, D_D_inf, label=system.split('/')[1], color=color, linewidth=2)
111
112
ax.set_xlabel('Time (Ga)')
113
ax.set_ylabel('$D^*/D^*_\\infty$')
114
ax.set_title('Daughter Isotope Growth')
115
ax.legend(fontsize=8)
116
ax.grid(True, alpha=0.3)
117
ax.set_xlim(0, 20)
118
119
# Half-life comparison
120
ax = axes[0, 2]
121
systems_all = list(isotope_systems.keys())
122
half_lives = [isotope_systems[s]['half_life'] for s in systems_all]
123
colors_all = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']
124
bars = ax.barh(range(len(systems_all)), np.log10(half_lives), color=colors_all, alpha=0.7)
125
ax.set_yticks(range(len(systems_all)))
126
ax.set_yticklabels([s.split('/')[0] for s in systems_all])
127
ax.set_xlabel('$\\log_{10}(t_{1/2}$ in years)')
128
ax.set_title('Half-life Comparison')
129
ax.grid(True, alpha=0.3, axis='x')
130
131
# Rb-Sr isochron
132
ax = axes[1, 0]
133
true_age_RbSr = 3.8e9
134
lambda_Rb = isotope_systems['Rb-87/Sr-87']['lambda']
135
Rb87_Sr86 = np.array([0.3, 0.8, 1.5, 2.5, 4.0, 6.0])
136
Sr87_Sr86_initial = 0.7045
137
Sr87_Sr86 = Sr87_Sr86_initial + Rb87_Sr86 * (np.exp(lambda_Rb * true_age_RbSr) - 1)
138
Sr87_Sr86_err = np.random.normal(0, 0.001, len(Sr87_Sr86))
139
Sr87_Sr86_meas = Sr87_Sr86 + Sr87_Sr86_err
140
141
slope_Rb, intercept_Rb, r_Rb, p_Rb, se_Rb = linregress(Rb87_Sr86, Sr87_Sr86_meas)
142
age_RbSr = isochron_age(slope_Rb, lambda_Rb)
143
144
ax.errorbar(Rb87_Sr86, Sr87_Sr86_meas, yerr=0.002, fmt='o', color='#2ecc71',
145
markersize=8, capsize=3, label='Samples')
146
fit_x = np.linspace(0, 7, 100)
147
ax.plot(fit_x, slope_Rb * fit_x + intercept_Rb, 'r-', linewidth=2, label='Isochron')
148
ax.set_xlabel('$^{87}$Rb/$^{86}$Sr')
149
ax.set_ylabel('$^{87}$Sr/$^{86}$Sr')
150
ax.set_title(f'Rb-Sr Isochron: {age_RbSr/1e9:.2f} Ga')
151
ax.legend()
152
ax.grid(True, alpha=0.3)
153
154
# K-Ar age spectrum
155
ax = axes[1, 1]
156
steps = np.arange(1, 11)
157
cumulative_Ar = np.cumsum(np.random.exponential(10, 10))
158
cumulative_Ar = cumulative_Ar / cumulative_Ar[-1] * 100
159
plateau_age = 2.5e9
160
ages = plateau_age + np.random.normal(0, 0.1e9, 10)
161
ages[0:2] *= 0.8 # Low-T steps often younger
162
ages[8:10] *= 1.1 # High-T steps may be older
163
164
ax.step(cumulative_Ar, ages/1e9, where='mid', linewidth=2, color='#f39c12')
165
ax.axhline(y=plateau_age/1e9, color='r', linestyle='--', alpha=0.7, label='Plateau')
166
ax.fill_between([20, 80], [plateau_age/1e9-0.1]*2, [plateau_age/1e9+0.1]*2,
167
alpha=0.2, color='red')
168
ax.set_xlabel('Cumulative $^{39}$Ar Released (\\%)')
169
ax.set_ylabel('Apparent Age (Ga)')
170
ax.set_title('Ar-Ar Age Spectrum')
171
ax.legend()
172
ax.grid(True, alpha=0.3)
173
ax.set_xlim(0, 100)
174
175
# C-14 calibration curve
176
ax = axes[1, 2]
177
C14_age = np.linspace(0, 50000, 500)
178
cal_age = C14_age * (1 + 0.05 * np.sin(C14_age/5000)) # Simplified calibration
179
cal_age += np.random.normal(0, 200, len(C14_age))
180
181
ax.plot(cal_age, C14_age, 'b-', linewidth=1.5, alpha=0.7)
182
ax.plot([0, 50000], [0, 50000], 'r--', alpha=0.5, label='1:1 line')
183
ax.set_xlabel('Calendar Age (years BP)')
184
ax.set_ylabel('$^{14}$C Age (years BP)')
185
ax.set_title('Radiocarbon Calibration')
186
ax.legend()
187
ax.grid(True, alpha=0.3)
188
189
plt.tight_layout()
190
plt.savefig('decay_kinetics.pdf', dpi=150, bbox_inches='tight')
191
plt.close()
192
\end{pycode}
193
194
\begin{figure}[htbp]
195
\centering
196
\includegraphics[width=0.95\textwidth]{decay_kinetics.pdf}
197
\caption{Radioactive decay: (a) parent decay curves, (b) daughter growth, (c) half-life comparison, (d) Rb-Sr isochron, (e) Ar-Ar spectrum, (f) C-14 calibration.}
198
\end{figure}
199
200
\chapter{U-Pb Concordia Dating}
201
202
\section{Concordia Equation}
203
The U-Pb concordia curve represents concordant ages:
204
\begin{align}
205
\frac{^{206}\text{Pb}^*}{^{238}\text{U}} &= e^{\lambda_{238} t} - 1 \\
206
\frac{^{207}\text{Pb}^*}{^{235}\text{U}} &= e^{\lambda_{235} t} - 1
207
\end{align}
208
209
\begin{pycode}
210
# U-Pb Concordia analysis
211
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
212
213
lambda_238 = isotope_systems['U-238/Pb-206']['lambda']
214
lambda_235 = isotope_systems['U-235/Pb-207']['lambda']
215
216
# Concordia curve
217
t_conc = np.linspace(0, 4.5e9, 1000)
218
Pb206_U238 = np.exp(lambda_238 * t_conc) - 1
219
Pb207_U235 = np.exp(lambda_235 * t_conc) - 1
220
221
ax = axes[0, 0]
222
ax.plot(Pb207_U235, Pb206_U238, 'k-', linewidth=2, label='Concordia')
223
224
# Age markers
225
ages_mark = [0.5e9, 1e9, 2e9, 3e9, 4e9]
226
for age in ages_mark:
227
x = np.exp(lambda_235 * age) - 1
228
y = np.exp(lambda_238 * age) - 1
229
ax.plot(x, y, 'ko', markersize=8)
230
ax.annotate(f'{age/1e9:.1f} Ga', xy=(x, y), xytext=(x+2, y+0.05),
231
fontsize=8)
232
233
# Discordant samples
234
upper_intercept = 3.5e9
235
lower_intercept = 0.5e9
236
n_samples = 6
237
fractions = np.linspace(0.1, 0.9, n_samples)
238
sample_ages = lower_intercept + fractions * (upper_intercept - lower_intercept)
239
sample_x = np.exp(lambda_235 * sample_ages) - 1 + np.random.normal(0, 0.5, n_samples)
240
sample_y = np.exp(lambda_238 * sample_ages) - 1 + np.random.normal(0, 0.02, n_samples)
241
242
ax.scatter(sample_x, sample_y, c='red', s=60, zorder=5, label='Zircons')
243
244
# Discordia line
245
slope_disc, intercept_disc = np.polyfit(sample_x, sample_y, 1)
246
disc_x = np.linspace(0, 80, 100)
247
ax.plot(disc_x, slope_disc * disc_x + intercept_disc, 'r--', linewidth=1.5,
248
alpha=0.7, label='Discordia')
249
250
ax.set_xlabel('$^{207}$Pb*/$^{235}$U')
251
ax.set_ylabel('$^{206}$Pb*/$^{238}$U')
252
ax.set_title('Concordia-Discordia Diagram')
253
ax.legend(loc='lower right')
254
ax.grid(True, alpha=0.3)
255
ax.set_xlim(0, 80)
256
ax.set_ylim(0, 1.2)
257
258
# Wetherill concordia (zoomed)
259
ax = axes[0, 1]
260
t_zoom = np.linspace(3e9, 4e9, 500)
261
Pb206_zoom = np.exp(lambda_238 * t_zoom) - 1
262
Pb207_zoom = np.exp(lambda_235 * t_zoom) - 1
263
264
ax.plot(Pb207_zoom, Pb206_zoom, 'k-', linewidth=2, label='Concordia')
265
266
# Concordant zircons
267
concordant_ages = np.array([3.2e9, 3.4e9, 3.5e9, 3.6e9, 3.8e9])
268
conc_x = np.exp(lambda_235 * concordant_ages) - 1 + np.random.normal(0, 0.2, 5)
269
conc_y = np.exp(lambda_238 * concordant_ages) - 1 + np.random.normal(0, 0.005, 5)
270
ax.scatter(conc_x, conc_y, c='green', s=60, zorder=5, label='Concordant')
271
272
# Error ellipses (simplified)
273
for x, y in zip(conc_x, conc_y):
274
ellipse = plt.Circle((x, y), 0.3, fill=False, color='green', alpha=0.5)
275
ax.add_patch(ellipse)
276
277
ax.set_xlabel('$^{207}$Pb*/$^{235}$U')
278
ax.set_ylabel('$^{206}$Pb*/$^{238}$U')
279
ax.set_title('Concordant Zircons (3-4 Ga)')
280
ax.legend()
281
ax.grid(True, alpha=0.3)
282
283
# Tera-Wasserburg diagram
284
ax = axes[1, 0]
285
U238_Pb206 = 1 / Pb206_U238[1:] # Avoid division by zero
286
Pb207_Pb206 = Pb207_U235[1:] / Pb206_U238[1:] * (1/137.88) # Using U isotope ratio
287
288
ax.plot(U238_Pb206, Pb207_Pb206, 'k-', linewidth=2)
289
for age in ages_mark:
290
if age > 0:
291
x = 1 / (np.exp(lambda_238 * age) - 1)
292
y = (np.exp(lambda_235 * age) - 1) / (np.exp(lambda_238 * age) - 1) / 137.88
293
ax.plot(x, y, 'ko', markersize=6)
294
295
ax.set_xlabel('$^{238}$U/$^{206}$Pb*')
296
ax.set_ylabel('$^{207}$Pb*/$^{206}$Pb*')
297
ax.set_title('Tera-Wasserburg Diagram')
298
ax.set_xlim(0, 20)
299
ax.grid(True, alpha=0.3)
300
301
# Pb-Pb isochron
302
ax = axes[1, 1]
303
mu_values = np.array([8, 9, 10, 11, 12]) # Different U/Pb sources
304
t_earth = 4.55e9
305
Pb204_initial = 1.0
306
307
Pb206_Pb204 = 9.307 + mu_values * (np.exp(lambda_238 * t_earth) - 1)
308
Pb207_Pb204 = 10.294 + mu_values/137.88 * (np.exp(lambda_235 * t_earth) - 1)
309
Pb206_Pb204 += np.random.normal(0, 0.1, len(mu_values))
310
Pb207_Pb204 += np.random.normal(0, 0.02, len(mu_values))
311
312
ax.scatter(Pb206_Pb204, Pb207_Pb204, c='#9b59b6', s=60)
313
slope_Pb, intercept_Pb = np.polyfit(Pb206_Pb204, Pb207_Pb204, 1)
314
fit_x = np.linspace(14, 22, 100)
315
ax.plot(fit_x, slope_Pb * fit_x + intercept_Pb, 'r-', linewidth=2)
316
317
# Calculate model age
318
Pb_age = (1/lambda_235) * np.log(1 + slope_Pb * 137.88 *
319
(np.exp(lambda_235 * t_earth) - 1) / (np.exp(lambda_238 * t_earth) - 1))
320
321
ax.set_xlabel('$^{206}$Pb/$^{204}$Pb')
322
ax.set_ylabel('$^{207}$Pb/$^{204}$Pb')
323
ax.set_title('Pb-Pb Isochron')
324
ax.grid(True, alpha=0.3)
325
326
plt.tight_layout()
327
plt.savefig('UPb_concordia.pdf', dpi=150, bbox_inches='tight')
328
plt.close()
329
330
# Store results
331
upper_age = upper_intercept
332
lower_age = lower_intercept
333
\end{pycode}
334
335
\begin{figure}[htbp]
336
\centering
337
\includegraphics[width=0.95\textwidth]{UPb_concordia.pdf}
338
\caption{U-Pb dating: (a) concordia-discordia, (b) concordant zircons, (c) Tera-Wasserburg, (d) Pb-Pb isochron.}
339
\end{figure}
340
341
\chapter{Closure Temperature}
342
343
\section{Dodson Equation}
344
The closure temperature $T_c$ depends on diffusion parameters:
345
\begin{equation}
346
T_c = \frac{E_a/R}{\ln\left(\frac{A R T_c^2 D_0/a^2}{E_a \cdot dT/dt}\right)}
347
\end{equation}
348
349
\begin{pycode}
350
# Closure temperature analysis
351
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
352
353
# Mineral closure temperatures
354
minerals = ['Zircon (U-Pb)', 'Monazite', 'Titanite', 'Hornblende',
355
'Muscovite', 'Biotite', 'K-feldspar', 'Apatite']
356
Tc = [900, 750, 650, 550, 400, 350, 200, 70]
357
colors_min = plt.cm.RdYlBu(np.linspace(0.9, 0.1, len(minerals)))
358
359
ax = axes[0]
360
bars = ax.barh(range(len(minerals)), Tc, color=colors_min, alpha=0.8)
361
ax.set_yticks(range(len(minerals)))
362
ax.set_yticklabels(minerals)
363
ax.set_xlabel('Closure Temperature ($^\\circ$C)')
364
ax.set_title('Mineral Closure Temperatures')
365
ax.grid(True, alpha=0.3, axis='x')
366
367
# Cooling path
368
ax = axes[1]
369
time = np.linspace(0, 100, 100) # Ma
370
T_path = 900 * np.exp(-time/30) # Exponential cooling
371
372
ax.plot(time, T_path, 'b-', linewidth=2)
373
for mineral, tc in zip(['Zircon', 'Hornblende', 'Biotite', 'Apatite'],
374
[900, 550, 350, 70]):
375
t_closure = -30 * np.log(tc/900)
376
if t_closure > 0:
377
ax.plot(t_closure, tc, 'ro', markersize=8)
378
ax.annotate(mineral, xy=(t_closure, tc), xytext=(t_closure+5, tc+30),
379
fontsize=8, arrowprops=dict(arrowstyle='->', alpha=0.5))
380
381
ax.set_xlabel('Time (Ma after crystallization)')
382
ax.set_ylabel('Temperature ($^\\circ$C)')
383
ax.set_title('Cooling Path and Closure Events')
384
ax.grid(True, alpha=0.3)
385
386
# Multi-chronometer cooling history
387
ax = axes[2]
388
systems_cool = ['U-Pb Zrn', 'Ar-Ar Hbl', 'Rb-Sr Ms', 'K-Ar Bt', 'FT Ap']
389
ages_cool = [100, 85, 60, 45, 20] # Ma
390
Tc_cool = [900, 550, 400, 350, 70]
391
392
ax.scatter(ages_cool, Tc_cool, s=100, c='#e74c3c', zorder=5)
393
for sys, age, tc in zip(systems_cool, ages_cool, Tc_cool):
394
ax.annotate(sys, xy=(age, tc), xytext=(age-10, tc+50),
395
fontsize=8, arrowprops=dict(arrowstyle='->', alpha=0.5))
396
397
# Fit cooling curve
398
from scipy.optimize import curve_fit
399
def cooling(t, T0, tau, T_amb):
400
return T_amb + (T0 - T_amb) * np.exp(-(100-t)/tau)
401
402
popt, _ = curve_fit(cooling, ages_cool, Tc_cool, p0=[1000, 30, 0], maxfev=5000)
403
t_fit = np.linspace(0, 100, 100)
404
ax.plot(t_fit, cooling(t_fit, *popt), 'b-', linewidth=2, alpha=0.7)
405
406
ax.set_xlabel('Age (Ma)')
407
ax.set_ylabel('Temperature ($^\\circ$C)')
408
ax.set_title('Thermochronology')
409
ax.grid(True, alpha=0.3)
410
ax.invert_xaxis()
411
412
plt.tight_layout()
413
plt.savefig('closure_temperature.pdf', dpi=150, bbox_inches='tight')
414
plt.close()
415
416
# Cooling rate
417
cooling_rate = (Tc_cool[0] - Tc_cool[-1]) / (ages_cool[0] - ages_cool[-1])
418
\end{pycode}
419
420
\begin{figure}[htbp]
421
\centering
422
\includegraphics[width=0.95\textwidth]{closure_temperature.pdf}
423
\caption{Closure temperature: (a) mineral comparison, (b) cooling path, (c) multi-system thermochronology.}
424
\end{figure}
425
426
\chapter{Numerical Results}
427
428
\begin{pycode}
429
# Compile results
430
results_table = [
431
('Rb-Sr isochron age', f'{age_RbSr/1e9:.3f}', 'Ga'),
432
('Initial $^{87}$Sr/$^{86}$Sr', f'{intercept_Rb:.4f}', ''),
433
('Isochron R$^2$', f'{r_Rb**2:.4f}', ''),
434
('U-Pb upper intercept', f'{upper_age/1e9:.2f}', 'Ga'),
435
('U-Pb lower intercept', f'{lower_age/1e9:.2f}', 'Ga'),
436
('Average cooling rate', f'{abs(cooling_rate):.1f}', '$^\\circ$C/Ma'),
437
]
438
\end{pycode}
439
440
\begin{table}[htbp]
441
\centering
442
\caption{Radiometric dating results}
443
\begin{tabular}{@{}lcc@{}}
444
\toprule
445
Parameter & Value & Units \\
446
\midrule
447
\py{results_table[0][0]} & \py{results_table[0][1]} & \py{results_table[0][2]} \\
448
\py{results_table[1][0]} & \py{results_table[1][1]} & \py{results_table[1][2]} \\
449
\py{results_table[2][0]} & \py{results_table[2][1]} & \py{results_table[2][2]} \\
450
\py{results_table[3][0]} & \py{results_table[3][1]} & \py{results_table[3][2]} \\
451
\py{results_table[4][0]} & \py{results_table[4][1]} & \py{results_table[4][2]} \\
452
\py{results_table[5][0]} & \py{results_table[5][1]} & \py{results_table[5][2]} \\
453
\bottomrule
454
\end{tabular}
455
\end{table}
456
457
\chapter{Conclusions}
458
459
\begin{enumerate}
460
\item Isochron dating provides both age and initial ratios
461
\item U-Pb concordia reveals open-system behavior
462
\item Multiple isotope systems enable cross-validation
463
\item Closure temperature controls age interpretation
464
\item Thermochronology constrains cooling histories
465
\item Radiocarbon requires calibration for calendar ages
466
\end{enumerate}
467
468
\end{document}
469
470