CSUMT Bostick Depth And Survey Design#
pycsamt.emtools.csumt provides two related workflows:
plan a controlled-source survey from target depths and an estimated background resistivity;
transform measured impedance data into quick Bostick depth and vertical-resolution diagnostics.
The module is intentionally lightweight. It does not run an inversion and it does not estimate a layered earth model. It gives you fast, transparent quantities that are useful before field acquisition and during early quality control of CSAMT/AMT lines.
Full callable signatures live in the API reference. This page focuses on practical use, interpretation, and reproducible examples.
The Bostick Transform#
The central approximation is:
where:
Dis Bostick depth in metres,rho_ais apparent resistivity in ohm-m,fis frequency in hertz.
The constant 356 comes from the skin-depth relation
503 * sqrt(rho / f) divided by sqrt(2). Lower frequencies probe
deeper. Higher apparent resistivity also pushes the estimated depth
deeper.
For measured EDI data, pyCSAMT first computes a determinant-style apparent resistivity from the two off-diagonal impedance modes:
That practical-unit formula matches the EDI impedance convention used
elsewhere in emtools.
When To Use This Page#
Use the planning side when you need to answer questions like:
what transmitter frequencies are needed for 10 m, 25 m, and 50 m targets?
does the CSUMT frequency band reach the required depth for the expected resistivity?
how much vertical resolution is lost between adjacent frequencies?
Use the measured-data side when you need to answer questions like:
which stations reach the deepest Bostick depths?
where does the line have poor vertical resolution?
do neighboring lines show similar depth-coverage patterns?
are absolute depth numbers plausible enough to guide inversion setup?
Planning With No EDI Data#
The planning helpers need only resistivity and frequency or target depth. The default CSUMT band used by the module is:
1f_min = 9.6e3 Hz
2f_max = 614.4e3 Hz
The first step is usually to plot the Bostick relation for plausible background resistivities.
1import matplotlib.pyplot as plt
2import numpy as np
3
4from pycsamt.emtools.csumt import (
5 F_MAX_CSUMT,
6 F_MIN_CSUMT,
7 bostick_depth_from_rho,
8)
9
10freq = np.logspace(2, 6, 200)
11resistivities = [30.0, 100.0, 300.0, 1000.0]
12
13fig, ax = plt.subplots(figsize=(7, 5))
14for rho in resistivities:
15 depth = bostick_depth_from_rho(rho, freq)
16 ax.loglog(freq, depth, label=f"rho={rho:g} ohm.m")
17
18ax.axvspan(F_MIN_CSUMT, F_MAX_CSUMT, color="0.9", zorder=0)
19ax.set_xlabel("Frequency (Hz)")
20ax.set_ylabel("Bostick depth (m)")
21ax.legend()
22fig.tight_layout()
Lines 5-9 import only the pure planning pieces. Line 16 evaluates the Bostick depth for each resistivity. Line 19 shades the default CSUMT band so you can see which depths are reachable by the instrument band.
Inverting Depth To Frequency#
Survey design often starts from target depths. frequency_for_depth
inverts the Bostick formula:
1import numpy as np
2
3from pycsamt.emtools.csumt import (
4 F_MAX_CSUMT,
5 F_MIN_CSUMT,
6 frequency_for_depth,
7)
8
9rho_estimate = 300.0
10targets_m = np.array([5.0, 10.0, 20.0, 35.0, 50.0, 75.0])
11
12freq_hz = frequency_for_depth(targets_m, rho_estimate)
13in_band = (freq_hz >= F_MIN_CSUMT) & (freq_hz <= F_MAX_CSUMT)
14
15for depth, freq, keep in zip(targets_m, freq_hz, in_band):
16 status = "inside CSUMT band" if keep else "outside CSUMT band"
17 print(f"{depth:5.1f} m -> {freq:9.1f} Hz {status}")
5.0 m -> 1520832.0 Hz outside CSUMT band
10.0 m -> 380208.0 Hz inside CSUMT band
20.0 m -> 95052.0 Hz inside CSUMT band
35.0 m -> 31037.4 Hz inside CSUMT band
50.0 m -> 15208.3 Hz inside CSUMT band
75.0 m -> 6759.3 Hz outside CSUMT band
Line 12 is the key conversion. Lines 13-17 force the practical check: does the requested target depth actually map into the transmitter band?
Designing A Frequency Schedule#
frequency_schedule converts target depths into transmitter
frequencies, removes depths that map outside the allowed band, and can
optionally add intermediate frequencies.
1import numpy as np
2
3from pycsamt.emtools.csumt import frequency_schedule
4
5rho_estimate = 300.0
6targets_m = np.array([10.0, 20.0, 35.0, 50.0, 65.0])
7
8schedule_hz = frequency_schedule(targets_m, rho_estimate)
9padded_hz = frequency_schedule(
10 targets_m,
11 rho_estimate,
12 min_resolution_m=5.0,
13)
14schedule_khz = frequency_schedule(
15 targets_m,
16 rho_estimate,
17 min_resolution_m=5.0,
18 as_khz=True,
19)
20
21print("requested targets:", len(targets_m))
22print("kept frequencies:", len(schedule_hz))
23print("padded frequencies:", len(padded_hz))
24print("padded schedule in kHz:", schedule_khz)
requested targets: 5
kept frequencies: 4
padded frequencies: 11
padded schedule in kHz: [ 15.20832 19.29075455 24.46905452 31.03738776 41.05860467
54.31542857 71.85255818 95.052 150.88564479 239.51603127
380.208 ]
The important behavior is line 8: targets outside f_min and
f_max are dropped from the result. The function returns the schedule;
it does not warn if a target is unreachable. Always compare the number
of requested target depths with the number of returned frequencies.
min_resolution_m inserts log-spaced frequencies between adjacent
schedule frequencies when their vertical resolution gap is too large.
as_khz=True only changes the units of the returned array.
Resolution Between Two Frequencies#
vertical_resolution_pair estimates the depth gap between two
adjacent frequencies for a fixed resistivity:
where f_lo is the lower frequency and therefore the deeper point.
1from pycsamt.emtools.csumt import vertical_resolution_pair
2
3rho_estimate = 300.0
4adjacent_pairs = [
5 (9600.0, 19200.0),
6 (19200.0, 38400.0),
7 (38400.0, 76800.0),
8]
9
10for f_lo, f_hi in adjacent_pairs:
11 delta_m = vertical_resolution_pair(rho_estimate, f_lo, f_hi)
12 print(f"{f_lo:8.0f}-{f_hi:8.0f} Hz: {delta_m:6.2f} m")
9600- 19200 Hz: 18.43 m
19200- 38400 Hz: 13.03 m
38400- 76800 Hz: 9.22 m
This is a planning calculation. It assumes the same background resistivity for both frequencies. Use it to design spacing before acquisition, or to compare measured resolution against an idealized background.
Measured Data Workflow#
The sites-based functions accept the usual emtools input:
a directory containing EDI files,
one EDI-like object,
a
Sitescontainer,an iterable of site-like objects.
Internally they call ensure_sites, so recursive, on_dup,
strict, and verbose behave consistently with the rest of the
user guide.
The measured workflow is:
compute one Bostick-depth row per station and frequency;
compute adjacent-frequency vertical resolution;
collapse each station to a coverage table;
plot a station x period depth pseudo-section.
1from pathlib import Path
2
3from pycsamt.emtools.csumt import (
4 bostick_depth,
5 depth_coverage_table,
6 plot_depth_section,
7 vertical_resolution,
8)
9
10edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
11
12depth = bostick_depth(edi_dir)
13resolution = vertical_resolution(edi_dir)
14coverage = depth_coverage_table(edi_dir)
15ax = plot_depth_section(edi_dir)
Lines 11-14 are the measured-data workflow. The same input path is accepted by each function.
Bostick Depth Table#
Use bostick_depth when you want the raw station-frequency transform.
1from pycsamt.emtools.csumt import bostick_depth
2
3depth = bostick_depth(
4 "data/AMT/WILLY_DATA/L18PLT",
5 recursive=True,
6 on_dup="replace",
7 strict=False,
8 verbose=0,
9)
10
11print(depth.head())
12depth.to_csv("l18plt_bostick_depth.csv", index=False)
station freq_hz period_s rho_a_ohmm depth_m
0 18-001A 10400.0 0.000096 76.998314 30.631900
1 18-001A 8707.0 0.000115 84.263304 35.021518
2 18-001A 7289.0 0.000137 88.743760 39.281217
3 18-001A 6102.0 0.000164 115.297433 48.935465
4 18-001A 5108.0 0.000196 152.193125 61.450026
The output columns are:
station: station name.freq_hz: measured frequency.period_s: inverse frequency.rho_a_ohmm: geometric-mean apparent resistivity fromZxyandZyx.depth_m: Bostick depth in metres.
One Station Curve#
A single-station curve is the easiest way to understand the transform before reading the full section.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.csumt import bostick_depth
4
5depth = bostick_depth("data/AMT/WILLY_DATA/L18PLT")
6
7station = "18-001A"
8one = depth.loc[depth["station"] == station].sort_values("period_s")
9
10fig, ax = plt.subplots(figsize=(7, 4.5))
11ax.loglog(one["period_s"], one["depth_m"], "o-")
12ax.set_xlabel("Period (s)")
13ax.set_ylabel("Bostick depth (m)")
14ax.set_title(f"{station} depth vs. period")
15ax.grid(True, which="both", alpha=0.3)
16fig.tight_layout()
Depth should generally increase with period, but it does not need to be perfectly smooth. Each point uses the apparent resistivity measured at that frequency. A noisy resistivity value can move one depth estimate above or below its neighbors.
Measured Vertical Resolution#
Use vertical_resolution to compute the depth gap between adjacent
frequencies at each station.
1from pycsamt.emtools.csumt import vertical_resolution
2
3measured = vertical_resolution("data/AMT/WILLY_DATA/L18PLT")
4fixed_rho = vertical_resolution(
5 "data/AMT/WILLY_DATA/L18PLT",
6 rho_override=300.0,
7)
8
9print(measured.head())
10print(fixed_rho.head())
station freq_lo_hz freq_hi_hz ... depth_hi_m delta_depth_m rho_a_ohmm
0 18-001A 1.008 1.204 ... 15344.900917 -1741.179129 1814.534975
1 18-001A 1.204 1.438 ... 11898.474477 3446.426441 1895.605946
2 18-001A 1.438 1.718 ... 11376.860958 521.613519 1678.822421
3 18-001A 1.718 2.052 ... 20671.989002 -9295.128044 3484.215253
4 18-001A 2.052 2.451 ... 13906.815678 6765.173324 5087.100314
[5 rows x 7 columns]
station freq_lo_hz freq_hi_hz ... depth_hi_m delta_depth_m rho_a_ohmm
0 18-001A 1.008 1.204 ... 5619.496200 522.087278 300.0
1 18-001A 1.204 1.438 ... 5141.989463 477.506737 300.0
2 18-001A 1.438 1.718 ... 4704.343719 437.645744 300.0
3 18-001A 1.718 2.052 ... 4304.492417 399.851302 300.0
4 18-001A 2.052 2.451 ... 3938.573638 365.918779 300.0
[5 rows x 7 columns]
The measured output columns are:
station: station name.freq_lo_hzandfreq_hi_hz: adjacent frequency pair, sorted ascending.depth_lo_manddepth_hi_m: Bostick depths at the pair.delta_depth_m:depth_lo_m - depth_hi_m.rho_a_ohmm: geometric-mean apparent resistivity used for the pair.
When rho_override is omitted, each pair uses measured apparent
resistivity. When rho_override is set, the calculation uses a fixed
background resistivity everywhere. That is useful for comparing field
data against an idealized survey-design assumption.
Depth Coverage Table#
Use depth_coverage_table when you want one row per station.
1from pycsamt.emtools.csumt import depth_coverage_table
2
3coverage = depth_coverage_table("data/AMT/WILLY_DATA/L18PLT")
4
5ranked = coverage.sort_values("depth_max_m", ascending=False)
6
7print(
8 ranked[
9 [
10 "station",
11 "n_freq",
12 "freq_min_hz",
13 "freq_max_hz",
14 "depth_min_m",
15 "depth_max_m",
16 "median_resolution_m",
17 ]
18 ].head(10)
19)
station n_freq ... depth_max_m median_resolution_m
12 18-013U 53 ... 36009.355793 64.592879
19 18-020A 53 ... 32408.420051 75.994660
21 18-021U 53 ... 30301.925460 79.090618
0 18-001A 53 ... 25738.828561 48.506018
20 18-021B 53 ... 22190.349906 52.988073
8 18-009A 53 ... 21357.805389 67.062557
26 18-024U 53 ... 20271.948048 12.972740
6 18-007U 53 ... 19380.271567 50.198288
7 18-008U 53 ... 19024.796081 50.476517
16 18-017U 53 ... 17389.552044 48.953237
[10 rows x 7 columns]
The output columns are:
n_freq: number of measured frequencies.freq_min_hzandfreq_max_hz: frequency range for the station.depth_min_manddepth_max_m: shallowest and deepest Bostick depths estimated at the station.mean_resolution_mandmedian_resolution_m: adjacent-frequency depth gaps summarized for the station.
The deepest station is not automatically the best station. Very high apparent resistivity at low frequency can create a large Bostick depth. Use the table as a ranking and a quality-control clue, then inspect the curves and pseudo-section.
Depth Pseudo-Section#
plot_depth_section colors station x period cells by Bostick depth.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.csumt import plot_depth_section
4
5edi_dir = "data/AMT/WILLY_DATA/L18PLT"
6
7fig, ax = plt.subplots(figsize=(10, 5))
8plot_depth_section(
9 edi_dir,
10 log_color=True,
11 sort_by="name",
12 period_axis=True,
13 ax=ax,
14)
15fig.tight_layout()
16fig.savefig("l18plt_bostick_depth_section.png", dpi=200)
Useful plotting options:
log_color=Truecolors bylog10(depth_m). This is the default and is usually easier to read when depths span orders of magnitude.sort_by="name"sorts stations by station name."lon"and"lat"are also supported when coordinates are available.period_axis=Trueuses period on the y-axis. Set it toFalsefor frequency.ax=...lets you place several lines in one figure.
Comparing Neighboring Lines#
Use a shared y-axis to compare two survey lines with the same depth color scale logic.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.csumt import depth_coverage_table, plot_depth_section
4
5l18 = "data/AMT/WILLY_DATA/L18PLT"
6l22 = "data/AMT/WILLY_DATA/L22PLT"
7
8fig, (ax18, ax22) = plt.subplots(1, 2, figsize=(12, 5), sharey=True)
9
10plot_depth_section(l18, ax=ax18)
11ax18.set_title("L18PLT")
12
13plot_depth_section(l22, ax=ax22)
14ax22.set_title("L22PLT")
15
16c18 = depth_coverage_table(l18)
17c22 = depth_coverage_table(l22)
18
19print("L18 mean max depth:", c18["depth_max_m"].mean())
20print("L22 mean max depth:", c22["depth_max_m"].mean())
21
22fig.tight_layout()
L18 mean max depth: 16171.310165020153
L22 mean max depth: 18017.08253563618
Neighboring lines should not be identical, but a severe mismatch is a reason to check station ordering, coordinate metadata, outlier resistivity values, and EDI quality before interpreting the difference as geology.
Reading The Results#
Use these rules of thumb:
Treat Bostick depth as a fast transform, not as an inversion result.
Prefer broad station-period patterns over one-frequency extremes.
Check
rho_a_ohmmwhendepth_mlooks surprisingly large.Compare measured vertical resolution with a fixed
rho_overrideif you need to separate survey-design spacing from resistivity effects.Use neighboring lines as sanity checks before making absolute depth claims.
Common Failure Modes#
- Empty tables
No valid impedance tensors were loaded. Check the EDI path and make sure files contain usable
Zdata.- Unreachable target depths
frequency_scheduledrops target depths that map outside the requested frequency band. Compare input target count with output schedule count.- Very large depth estimates
Bostick depth grows with apparent resistivity and decreases with frequency. High-resistivity, low-frequency outliers can produce large depths that should be treated as qualitative indicators.
- Negative or surprising resolution gaps
With measured data, adjacent depths are computed from adjacent measured resistivities. Noise or strong resistivity variation can make the sequence less smooth than the analytical fixed-resistivity formula.
Saving A Reproducible Bundle#
For reporting, save the planning assumptions, measured tables, and key figure together.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4import numpy as np
5
6from pycsamt.emtools.csumt import (
7 bostick_depth,
8 depth_coverage_table,
9 frequency_schedule,
10 plot_depth_section,
11 vertical_resolution,
12)
13
14survey = Path("data/AMT/WILLY_DATA/L18PLT")
15out = Path("outputs/csumt_l18plt")
16out.mkdir(parents=True, exist_ok=True)
17
18targets_m = np.array([10.0, 20.0, 35.0, 50.0])
19schedule = frequency_schedule(targets_m, rho_estimate=300.0)
20
21np.savetxt(out / "planned_frequency_schedule_hz.txt", schedule)
22bostick_depth(survey).to_csv(out / "bostick_depth.csv", index=False)
23vertical_resolution(survey).to_csv(out / "vertical_resolution.csv", index=False)
24depth_coverage_table(survey).to_csv(out / "depth_coverage.csv", index=False)
25
26fig, ax = plt.subplots(figsize=(10, 5))
27plot_depth_section(survey, ax=ax)
28fig.tight_layout()
29fig.savefig(out / "bostick_depth_section.png", dpi=200)
Lines 19-21 preserve the design assumptions. Lines 22-24 save the measured data products. Lines 26-29 save the figure used in the report.
Worked Example#
The gallery example applies the Bostick transform to L18PLT and
L22PLT from data/AMT/WILLY_DATA/. It starts with the pure
planning equations, then moves to measured station curves, station
coverage, pseudo-sections, and a two-line comparison.
Open the rendered example here: Bostick depth and CSUMT survey design (pycsamt.emtools.csumt).