12.5. Structural Quality Control#
pycsamt.airborne.qc assesses representation quality,
completeness, and metadata consistency for an
AirborneEMDataset – how much navigation
has an attached response, whether a fixed ground reference is present
where the technology genuinely needs one, whether a flight line’s
frequency axis is even physically valid – without inventing a
universal geophysical threshold for signal-to-noise ratio, frequency
coverage, or anomaly quality. Those judgments are technology-specific
and belong in pycsamt.emtools, built on top of this common
structural layer rather than duplicated inside it. Every name below
imports from the top-level pycsamt.airborne package.
That restraint shows up directly in how findings are scored.
assess_airborne_qc() classifies a finding as
"error" only when the data are internally inconsistent in a way
nothing downstream could sensibly interpret – today, exactly one
thing: a non-positive or non-finite frequency axis. A missing EM
record, absent coordinates, or missing reference-station metadata can
matter a great deal, but they describe incomplete data, not
invalid data, so they are reported at "info"/"warning"
severity instead. Knowing that distinction in advance makes the
report’s status legible
without having to read every finding: "error" means something is
scientifically broken, "warning"/"pass" means the data are
merely as complete (or not) as they are.
12.5.1. A Compact Inventory#
inspect_airborne() accepts a dataset, line,
record, or bare EMTF document and returns one
AirborneInspection, filling in only the
fields meaningful at that level:
>>> from pycsamt.airborne import ensure_asites, inspect_airborne
>>> site = ensure_asites("data/ZTEM/gold_springs_nv")[0]
>>> inspect_airborne(site.record)
AirborneInspection(object_type='record', technologies=tuple(['ztem']), n_lines=0, n_samples=0, n_records=1, transfer_function_names=tuple(['tipper']), bbox=None, attrs=dict(len=1, keys=[sample_id]))
>>> inspect_airborne(site.emtf).attrs
{'product_id': 'gold_springs_nv_L1.GO_L1_001', 'subtype': 'ztem'}
A record’s inventory has no line or sample counts to speak of –
n_lines and n_samples stay at their defaults – while
n_records and transfer_function_names are meaningful at every
level, up to and including a whole dataset. Building one from a full
survey means first grouping the flat
AirborneSites view The Airborne Site View reads
back into the line/dataset structure
assess_airborne_qc() actually consumes – a
short, reusable step:
>>> import numpy as np
>>> from pycsamt.airborne import NavigationTrack, AirborneEMLine, AirborneEMDataset
>>> def make_line(line_label, members):
... nav = NavigationTrack(
... sample_ids=tuple(s.sample_id for s in members),
... latitude=np.array([s.coords[0] for s in members]),
... longitude=np.array([s.coords[1] for s in members]),
... terrain_elevation=np.array([s.coords[2] for s in members]),
... )
... line = AirborneEMLine(line_id=f"gold_springs_nv_{line_label}", navigation=nav)
... for s in members:
... line.add_emtf(s.sample_id, s.emtf)
... return line
>>> ztem_sites = ensure_asites("data/ZTEM/gold_springs_nv")
>>> line_labels = sorted(set(s.name.split("_")[1] for s in ztem_sites))
>>> lines = {
... f"gold_springs_nv_{lbl}": make_line(
... lbl, [s for s in ztem_sites if s.name.split("_")[1] == lbl],
... )
... for lbl in line_labels
... }
>>> clean_dataset = AirborneEMDataset(name="gold_springs_nv", lines=lines)
>>> insp = inspect_airborne(clean_dataset)
>>> insp.object_type, insp.technologies, insp.n_lines, insp.n_samples, insp.n_records
('dataset', ('ztem',), 7, 105, 105)
12.5.2. A Clean Survey#
Running assess_airborne_qc() on that same,
untouched real survey establishes what a genuinely complete submission
looks like – the baseline every later finding below is a deliberate
departure from:
>>> from pycsamt.airborne import assess_airborne_qc
>>> clean_report = assess_airborne_qc(clean_dataset)
>>> clean_report.status
'pass'
>>> len(clean_report.issues)
0
>>> clean_report.metrics["record_coverage_fraction"]
1.0
>>> clean_report.metrics["reference_metadata_fraction"]
1.0
>>> clean_report.metrics["variance_tf_fraction"]
1.0
>>> clean_report.metrics["inverse_signal_covariance_tf_fraction"]
0.0
Every navigation sample has an attached record, every ZTEM record
carries its fixed-ground reference-station tag, and every primary
transfer function has a variance estimate attached – this synthetic
survey was built to be complete. The last line is the honest
exception: inverse_signal_covariance_tf_fraction is 0.0 because
none of these records carry a full inverse-signal-covariance matrix,
which is normal for a variance-only delivery and is reported as a
plain metric rather than manufactured into a warning nobody asked for.
12.5.3. Five Real Findings#
Nothing above exercises the interesting code paths, because nothing
is actually wrong with gold_springs_nv. Building a second, smaller
dataset out of the same real records – one navigation sample with
no attached record, one record with its reference-station metadata
stripped, one record with a corrupted frequency axis, and one real
AFMAG record spliced onto an otherwise all-ZTEM line – exercises five
independent finding codes at once, each from a genuine structural
defect rather than a fabricated one:
>>> import copy
>>> l1_members = [s for s in ztem_sites if s.name.split("_")[1] == "L1"]
>>> l2_members = [s for s in ztem_sites if s.name.split("_")[1] == "L2"]
>>> line1 = make_line("L1", l1_members)
>>> _ = line1.records.pop("GO_L1_008")
>>> line2 = make_line("L2", l2_members)
>>> no_ref_doc = copy.deepcopy(line2.get_record("GO_L2_003").emtf)
>>> no_ref_doc.processing.remote_reference = None
>>> _ = line2.add_emtf("GO_L2_003", no_ref_doc, replace=True)
>>> bad_freq_doc = copy.deepcopy(line2.get_record("GO_L2_010").emtf)
>>> bad_freq_doc.periods = bad_freq_doc.periods.copy()
>>> bad_freq_doc.periods[0] = -1.0
>>> _ = line2.add_emtf("GO_L2_010", bad_freq_doc, replace=True)
>>> afmag_site = ensure_asites("data/AFMAG/abitibi_on")[0]
>>> line2.navigation = NavigationTrack(
... sample_ids=tuple(line2.navigation.sample_ids) + ("AB_001",),
... latitude=np.append(line2.navigation.latitude, afmag_site.coords[0]),
... longitude=np.append(line2.navigation.longitude, afmag_site.coords[1]),
... terrain_elevation=np.append(
... line2.navigation.terrain_elevation, afmag_site.coords[2],
... ),
... )
>>> _ = line2.add_emtf("AB_001", afmag_site.emtf)
>>> messy_dataset = AirborneEMDataset(
... name="gold_springs_nv_demo",
... lines={"gold_springs_nv_L1": line1, "gold_springs_nv_L2": line2},
... )
>>> messy_report = assess_airborne_qc(messy_dataset)
>>> messy_report.status
'error'
>>> for issue in messy_report.issues:
... print(f"[{issue.severity:<7}] {issue.code:<25} line={issue.line_id!r} sample={issue.sample_id!r}")
[info ] missing_em_records line='gold_springs_nv_L1' sample=None
[warning] mixed_line_technology line='gold_springs_nv_L2' sample=None
[warning] missing_reference_station line='gold_springs_nv_L2' sample='GO_L2_003'
[error ] invalid_frequency_axis line='gold_springs_nv_L2' sample='GO_L2_010'
[info ] mixed_dataset_technology line=None sample=None
Each finding traces back to exactly one deliberate change. Removing
GO_L1_008’s record but leaving its navigation sample in place –
line.records.pop(...) rather than rebuilding the line without it
– produces missing_em_records scoped to line L1 alone. Setting
processing.remote_reference to None on a copy of
GO_L2_003’s document reproduces exactly the check
Technologies, Formats, and Native I/O describes: ZTEM is one of the technologies
whose reference_required contract is True, so a ZTEM record
with no reference tag is flagged, sample-scoped. Corrupting
GO_L2_010’s frequency axis with one negative period is the only
"error"-severity finding here, exactly as the module’s severity
philosophy above promises – everything else is incomplete, this
alone is invalid. Splicing a real AFMAG record onto a ZTEM line’s
navigation produces two distinct findings from one change: line L2
itself is flagged as mixed_line_technology, and because that
mixture is now visible at dataset scope too,
identify_airborne_technologies() – the same
function Technologies, Formats, and Native I/O uses directly – also drives
mixed_dataset_technology on the whole report.
errors and
warnings filter
issues by severity, and
line_metrics keeps the
same coverage-fraction metrics metrics
reports dataset-wide, but broken out per line:
>>> len(messy_report.errors), len(messy_report.warnings)
(1, 2)
>>> messy_report.line_metrics["gold_springs_nv_L1"]["record_coverage_fraction"]
0.9333333333333333
>>> messy_report.line_metrics["gold_springs_nv_L2"]["transfer_function_names"]
('afmag_tilt', 'tipper')
L1’s coverage fraction, 14/15, isolates exactly which line
lost a record without having to search the dataset-wide fraction for
it, and L2’s transfer-function names show the AFMAG splice from a
completely different angle than the issues list did: this line now
carries two distinct transfer-function types where a genuine ZTEM
line would carry exactly one. Neither of these two examples is
exhaustive – a line with no finite navigation coordinates at all
raises a sixth code, missing_navigation_coordinates, not
reproduced here because every sample in both surveys used above
already has a real position.