Step 2: Build and Review QC Inventory
This notebook creates the QC tables used to decide which records can move into metric calculations. Observed and synthetic records are tracked separately, and a comparison is only eligible when both sides pass for the requested metric, component, and period band.
Imports
These helpers build QC rows, create comparison-ready records, export a manual-review queue, launch the QC dashboard, and make QC overview figures.
[1]:
from spatial_vtk.config.notebook import notebook_timer, register_svtk_cell_timer
with notebook_timer():
from spatial_vtk.config import SpatialVTKConfig
from spatial_vtk.config.metrics import metrics_settings_from_config
from spatial_vtk.config.outputs import resolve_output_path
from spatial_vtk.io import load_output_table, waveform_preprocessing_label, write_output_table
from spatial_vtk.qc import (
build_comparison_eligibility,
build_event_station_pair_retention_table,
build_retention_figure_table,
build_metric_qc_summary,
build_post_qc_record_table,
build_qc_waveform_comparison_records,
build_waveform_qc_summary,
export_manual_review_queue,
)
from spatial_vtk.visualize.dashboard import launch_qc_dashboard
from spatial_vtk.visualize.waveforms import plot_event_trace_comparison
from spatial_vtk.visualize.qc import (
plot_event_station_retention_heatmap,
plot_post_qc_station_event_map,
plot_qc_drop_cause_diagnostics,
plot_retention_summary,
)
register_svtk_cell_timer()
Run time: 1.57 s
Configuration
Load the tutorial config and set the output folder for this QC step.
[2]:
from pathlib import Path
# Use the repository root so paths match the public source checkout.
from spatial_vtk.tutorials import tutorial_root
repo_root = tutorial_root()
# Metadata table paths are project-relative.
import os
os.chdir(repo_root)
config_path = repo_root / "data/examples/configuration/example_spatial_vtk_config.yaml"
# Load the tutorial run scenario and make it the active config for later package calls.
cfg = SpatialVTKConfig.from_file(config_path, run_scenario="tutorial").activate()
notebook_overrides = {"qc_dashboard_port": 8502}
Run time: 13.1 ms
Load the Step 1 Files
Start from the prepared station, event, and event-station tables written by the first notebook.
[3]:
# Read the prepared station table written by Step 1.
stations = load_output_table("prepared_stations")
# Read the prepared event table written by Step 1.
events = load_output_table("prepared_events")
# Read the event-station record table written by Step 1.
event_stations = load_output_table("event_station_records")
print(f"Prepared stations: {len(stations)}")
print(f"Prepared events: {len(events)}")
print(f"Event-station records: {len(event_stations)}")
Prepared stations: 30
Prepared events: 5
Event-station records: 150
Run time: 57.1 ms
Use the QC helpers to inspect the observed and synthetic traces, then apply those decisions to the metric-level QC table. Observed and synthetic records are checked separately. A record is available for comparison only when both sides pass.
[4]:
# Resolve metrics, components, passbands, and spectral settings from the active config.
metric_settings = metrics_settings_from_config()
# Check the processed observed and synthetic waveform files against the configured QC rules.
trace_qc_summary = build_waveform_qc_summary(
event_stations,
components=metric_settings.components,
passbands=metric_settings.passbands,
verbose=True,
checkpoint_path=resolve_output_path("qc_trace_summary", kind="table", create_parent=True),
)
# Save the waveform-level QC table so you can inspect trace-level decisions directly.
write_output_table("qc_trace_summary", trace_qc_summary)
# Passband QC applies to ordinary metrics. PSA/FAS receive independent common-window checks in Step 3.
qc_summary = build_metric_qc_summary(
event_stations,
metrics=tuple(m for m in metric_settings.metrics if m not in {"PSA", "FAS"}),
components=metric_settings.components,
passbands=metric_settings.passbands,
spectral_periods_s=metric_settings.spectral.periods_s,
synthetic_max_frequency_hz=metric_settings.synthetic_max_frequency_hz,
trace_qc_summary=trace_qc_summary,
verbose=True,
checkpoint_path=resolve_output_path("qc_inventory", kind="table", create_parent=True),
resume=False, # Rebuild metric rows if metric/period settings changed.
)
# Save the QC inventory so later notebooks can filter metric calculations.
write_output_table("qc_inventory", qc_summary)
qc_summary.head()
Waveform QC: 150 event-station row(s), 2 source(s), 3 component(s), 2 passband(s)
Waveform QC: source 'observed' using column '__svtk_observed_waveform_path'
Waveform QC: source 'observed' checkpoint path /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_trace_summary.observed.checkpoint.csv
Trace QC observed: 150 event-station record(s), 3 component(s), 2 passband(s)
Trace QC observed: checkpoint path /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_trace_summary.observed.checkpoint.csv
Trace QC observed: no completed component groups found; all work is new
Trace QC observed: record 1/150 (elapsed 0.0s, 159.88 records/s, ETA 0.9s)
Trace QC observed: record 25/150 (elapsed 0.3s, 85.70 records/s, ETA 1.5s)
Trace QC observed: record 50/150 (elapsed 0.6s, 79.38 records/s, ETA 1.3s)
Trace QC observed: record 75/150 (elapsed 0.9s, 82.44 records/s, ETA 0.9s)
Trace QC observed: record 100/150 (elapsed 1.2s, 84.68 records/s, ETA 0.6s)
Trace QC observed: record 125/150 (elapsed 1.5s, 83.82 records/s, ETA 0.3s)
Trace QC observed: record 150/150 (elapsed 1.8s, 84.17 records/s, ETA 0.0s)
Trace QC observed: built 900 row(s) in 1.8s
Waveform QC: source 'observed' complete (900 row(s), elapsed 1.8s)
Waveform QC: source 'synthetic' using column '__svtk_synthetic_waveform_path'
Waveform QC: source 'synthetic' checkpoint path /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_trace_summary.synthetic.checkpoint.csv
Trace QC synthetic: 150 event-station record(s), 3 component(s), 2 passband(s)
Trace QC synthetic: checkpoint path /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_trace_summary.synthetic.checkpoint.csv
Trace QC synthetic: no completed component groups found; all work is new
Trace QC synthetic: record 1/150 (elapsed 0.0s, 172.33 records/s, ETA 0.9s)
Trace QC synthetic: record 25/150 (elapsed 1.1s, 22.99 records/s, ETA 5.4s)
Trace QC synthetic: record 50/150 (elapsed 2.3s, 22.16 records/s, ETA 4.5s)
Trace QC synthetic: record 75/150 (elapsed 3.4s, 22.08 records/s, ETA 3.4s)
Trace QC synthetic: record 100/150 (elapsed 4.5s, 22.11 records/s, ETA 2.3s)
Trace QC synthetic: record 125/150 (elapsed 5.7s, 22.01 records/s, ETA 1.1s)
Trace QC synthetic: record 150/150 (elapsed 6.7s, 22.29 records/s, ETA 0.0s)
Trace QC synthetic: built 900 row(s) in 6.8s
Waveform QC: source 'synthetic' complete (900 row(s), elapsed 6.8s)
Waveform QC: built 1800 row(s)
Metric QC: loading event-station records
Metric QC: loaded 150 event-station record(s)
Metric QC: indexing trace QC dataframe with 1800 row(s)
Metric QC: loaded 1800 trace QC row(s) in 0.0s
Metric QC: building trace QC lookup
Metric QC: indexed 1800 trace QC group(s) in 0.0s
Metric QC: reset checkpoint /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_inventory.csv
Metric QC: 150 event-station record(s), 2 source(s), 3 component(s), 3 metric(s)
Metric QC: checkpoint path /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_inventory.csv
Metric QC: no completed event-station records found; all work is new
Metric QC: record 1/150 (elapsed 0.0s, 37.00 records/s, ETA 4.0s)
Metric QC: record 25/150 (elapsed 0.1s, 409.24 records/s, ETA 0.3s)
Metric QC: record 50/150 (elapsed 0.1s, 451.65 records/s, ETA 0.2s)
Metric QC: record 75/150 (elapsed 0.2s, 465.63 records/s, ETA 0.2s)
Metric QC: record 100/150 (elapsed 0.2s, 471.96 records/s, ETA 0.1s)
Metric QC: record 125/150 (elapsed 0.3s, 474.83 records/s, ETA 0.1s)
Metric QC: record 150/150 (elapsed 0.3s, 477.33 records/s, ETA 0.0s)
Metric QC: built 5400 row(s) in 0.3s
[4]:
| source | event_id | station | event_title | event_lat | event_lon | station_lat | station_lon | network | magnitude | ... | metric | period_s | qc_status | qc_reason | trace_start_s | sample_interval_s | valid_start_rel_s | valid_end_rel_s | valid_start_sample | valid_end_sample | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | observed | ci38038071 | BFS | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | ... | PGA | NaN | pass | -23.33 | 0.01 | -5.44 | 316.6 | 1789.0 | 33993.0 | |
| 1 | observed | ci38038071 | BFS | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | ... | PGA | NaN | pass | -23.33 | 0.01 | -5.44 | 316.6 | 1789.0 | 33993.0 | |
| 2 | observed | ci38038071 | BFS | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | ... | PGV | NaN | pass | -23.33 | 0.01 | -5.44 | 316.6 | 1789.0 | 33993.0 | |
| 3 | observed | ci38038071 | BFS | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | ... | PGV | NaN | pass | -23.33 | 0.01 | -5.44 | 316.6 | 1789.0 | 33993.0 | |
| 4 | observed | ci38038071 | BFS | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | ... | PGD | NaN | pass | -23.33 | 0.01 | -5.44 | 316.6 | 1789.0 | 33993.0 |
5 rows × 24 columns
Run time: 9.17 s
Find Comparison-Eligible Records
These passband decisions cover ordinary metrics; PSA/FAS support is reported with the lowpass results in Step 3. A row is comparison-ready when the observed side and synthetic side both pass for the same event, station, component, passband, metric, and period.
[5]:
# Keep only event-station-component-passband rows where observed and synthetic data both pass QC.
comparison_eligible = build_comparison_eligibility(qc_summary)
# Save the comparison-eligible records for the metric workflow.
write_output_table("comparison_eligible_records", comparison_eligible)
comparison_eligible.head()
[5]:
| source_observed | event_id | station | component | passband | metric_group | metric | period_s | qc_status_observed | qc_reason_observed | ... | valid_start_sample_synthetic | valid_end_sample_synthetic | event_title | event_lat | event_lon | station_lat | station_lon | network | magnitude | distance_km | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | observed | ci38038071 | BFS | Z | 1-2 sec | amplitude | PGA | NaN | pass | ... | 4354.0 | 82723.0 | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | 15.60501 | |
| 1 | observed | ci38038071 | BFS | Z | 2-3 sec | amplitude | PGA | NaN | pass | ... | 4354.0 | 82723.0 | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | 15.60501 | |
| 2 | observed | ci38038071 | BFS | Z | 1-2 sec | amplitude | PGV | NaN | pass | ... | 4354.0 | 82723.0 | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | 15.60501 | |
| 3 | observed | ci38038071 | BFS | Z | 2-3 sec | amplitude | PGV | NaN | pass | ... | 4354.0 | 82723.0 | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | 15.60501 | |
| 4 | observed | ci38038071 | BFS | Z | 1-2 sec | amplitude | PGD | NaN | pass | ... | 4354.0 | 82723.0 | ci38038071 | 34.136333 | -117.774667 | 34.239 | -117.659 | CI | 4.38 | 15.60501 |
5 rows × 33 columns
Run time: 87.8 ms
Make QC Figures
These figures summarize QC pass/fail counts, retained comparison pairs by event and station, station-event coverage after QC, and reasons records were dropped.
[6]:
# Summarize pass/fail counts by period band.
retention_table = build_retention_figure_table(qc_summary)
# Summarize retained observed/synthetic comparison pairs for each station-event pair.
event_station_retention = build_event_station_pair_retention_table(qc_summary)
# Join post-QC decisions back to event and station coordinates for mapping.
post_qc_records = build_post_qc_record_table(event_stations, events=events, qc_summary=qc_summary)
# QC retention summary: pass/fail counts by period band.
retention_fig = plot_retention_summary(
retention_table,
title="QC Retention Summary",
showfig=True,
savefig=True,
)
# Event-station heatmap: percentage of comparison pairs retained across components, passbands, and metrics.
availability_fig = plot_event_station_retention_heatmap(
event_station_retention,
title="Post-QC Pair Retention by Event and Station",
showfig=True,
savefig=True,
)
# Post-QC map: where accepted and rejected event-station records are located.
post_qc_fig = plot_post_qc_station_event_map(post_qc_records, add_basemap=True, showfig=True, savefig=True)
# Drop-cause diagnostics: the main reasons records were removed.
drop_cause_fig = plot_qc_drop_cause_diagnostics(qc_summary, showfig=True, savefig=True)
Run time: 3.42 s
Inspect QC-Passed Waveforms
Before opening the dashboard, plot a small set of retained R-component observed/synthetic waveform pairs. These are the processed waveforms that passed QC, limited to nearby stations so you can quickly check whether the traces look comparable.
[7]:
# Load QC-passed R-component observed/synthetic waveform pairs within 50 km.
waveform_preview = build_qc_waveform_comparison_records(
event_stations,
comparison_eligible=comparison_eligible,
component="R",
max_distance_km=50,
max_records=12,
)
assert not waveform_preview.empty, "No waveform pairs: check Step 1 inputs and Step 2 QC."
# Plot unnormalized waveforms with boosted automatic display gain so relative observed/synthetic amplitudes are preserved.
waveform_comparison_fig = plot_event_trace_comparison(
waveform_preview,
normalize=False,
amplitude_gain="auto",
amplitude_gain_multiplier=2.5,
distance_limit_km=None,
time_limit_s=60,
title="Post-QC R-Component Observed and Synthetic Waveforms",
filter_label=f"{waveform_preprocessing_label()}; unnormalized traces; display gain auto x2.5",
showfig=True,
savefig=True,
)
print(f"Waveform pairs shown: {len(waveform_preview)}")
Waveform pairs shown: 12
Run time: 1.03 s
Open the QC Dashboard
The QC dashboard opens with the table you just wrote. The Manual Review Queue tab lets you inspect the queue and download the picker-ready CSV.
[8]:
# Export a manual-review queue that the manual QC picker can read.
manual_queue_path = export_manual_review_queue(qc_summary)
# Launch explicitly in a terminal; notebook execution does not start background servers.
print(f"svtk dashboard qc --trace-summary {resolve_output_path('qc_trace_summary', kind='table')} --port 8502")
print(f"Manual-review queue: {manual_queue_path.relative_to(repo_root)}")
svtk dashboard qc --trace-summary /home/runner/work/spatial-vtk/spatial-vtk/outputs/tutorials/tables/qc_trace_summary.csv --port 8502
Manual-review queue: outputs/tutorials/tables/manual_review_queue.csv
Run time: 145.8 ms