12.2. Flight Lines and Datasets#
NavigationTrack,
AirborneEMRecord,
AirborneEMLine, and
AirborneEMDataset are the four containers
every technology subpackage builds on. Navigation is the definitive
spine: a flight line’s sample identifiers, position, and attitude are
recorded once, independently of whether every sample actually
produced a usable transfer function. Records are then attached
sparsely, keyed by the same sample identifiers, so a rejected or
missing EM sample never forces deleting its navigation point, and
never gets a fabricated response to fill the gap. Every name below
imports from the top-level pycsamt.airborne package; this page
builds everything from scratch with small synthetic examples, since
reading a real committed survey through this same model is
The Airborne Site View’s job.
12.2.2. Records and Lines#
An AirborneEMLine pairs one
NavigationTrack with a sparse
{sample_id: AirborneEMRecord} mapping. A freshly built line has
navigation but no records at all –
missing_sample_ids reports
every sample as missing until records are actually attached:
>>> from pycsamt.airborne import AirborneEMLine
>>> line = AirborneEMLine(line_id="DEMO01", navigation=nav)
>>> line.n_samples, line.n_records
(20, 0)
>>> len(line.missing_sample_ids)
20
build_ztem_record() – the same kind of
technology constructor The Airborne Site View and Technologies, Formats, and Native I/O already
use – builds one AirborneEMRecord at a
time, ready for add_record().
Deliberately skipping sample S07 leaves the line genuinely sparse,
not just theoretically capable of it:
>>> from pycsamt.airborne.ztem import build_ztem_record, ZTEMSystemSpec
>>> freqs = np.array([90.0, 180.0, 360.0])
>>> for i, sid in enumerate(nav.sample_ids):
... if i == 7:
... continue
... tip = np.zeros((3, 2), dtype=complex)
... tip[:, 0] = 0.05 + 0.01j
... tip[:, 1] = 0.02 - 0.005j
... record = build_ztem_record(sid, tip, frequency=freqs, system_spec=ZTEMSystemSpec())
... _ = line.add_record(record)
>>> line.n_records
19
>>> line.missing_sample_ids
('S07',)
>>> line.transfer_function_names
('tipper',)
>>> line.record_at(0).sample_id
'S00'
>>> line.record_at(7) is None
True
record_at() and
get_record() both return
None for a missing sample rather than raising – S07 is a
perfectly valid navigation index, it simply has nothing attached.
iter_records() skips it
silently and yields every other record in navigation order, not
insertion order, which happens to be the same order here only because
records were added in navigation order to begin with:
>>> order = [r.sample_id for r in line.iter_records()]
>>> order == [s for s in nav.sample_ids if s != "S07"]
True
12.2.3. Assembling A Dataset#
AirborneEMDataset collects lines the same
way lines collect records – keyed, this time by line_id – and
adds survey-level bookkeeping on top: iterating every line or every
record across the whole survey, and recovering just the EMTF payloads
that actually exist. Two more small lines, offset 100 m apart along
northing, make that concrete:
>>> from pycsamt.airborne import AirborneEMDataset
>>> def make_offset_line(line_id, northing_offset):
... nav2 = NavigationTrack(
... sample_ids=tuple(f"{line_id}_S{i:02d}" for i in range(n)),
... easting=x, northing=np.full(n, northing_offset),
... terrain_elevation=terrain, platform_elevation=platform,
... )
... ln = AirborneEMLine(line_id=line_id, navigation=nav2)
... for sid in nav2.sample_ids:
... tip = np.zeros((3, 2), dtype=complex)
... tip[:, 0] = 0.05 + 0.01j
... tip[:, 1] = 0.02 - 0.005j
... rec = build_ztem_record(sid, tip, frequency=freqs, system_spec=ZTEMSystemSpec())
... ln.add_record(rec)
... return ln
>>> line_a = make_offset_line("A", 0.0)
>>> line_b = make_offset_line("B", 100.0)
>>> dataset = AirborneEMDataset(name="demo_survey", lines={"A": line_a, "B": line_b})
>>> dataset.n_lines, dataset.n_samples, dataset.n_records
(2, 40, 40)
>>> dataset.line_ids
('A', 'B')
>>> dataset.get_line("A") is line_a
True
>>> _ = dataset.add_line(line, replace=True)
>>> dataset.n_lines
3
>>> len(dataset.emtf_records())
59
59, not 60 – dataset.emtf_records() folds DEMO01’s
own sparsity into the dataset total automatically, since
emtf_records() omits any
record with no attached EMTF rather than representing the gap
with a placeholder, the same convention
iter_records() already applies
at the line level.
inspect()/
qc() are thin, lazily-
imported convenience wrappers around exactly the functions
Structural Quality Control covers directly –
inspect_airborne()/
assess_airborne_qc() – so a dataset can
inspect or assess itself without an extra import:
>>> insp = dataset.inspect()
>>> insp.object_type, insp.n_lines, insp.n_samples, insp.n_records
('dataset', 3, 60, 59)
>>> report = dataset.qc()
>>> report.status
'warning'
>>> len(report.warnings), len(report.errors)
(59, 0)
Fifty-nine warnings – one per attached record across all three
lines, DEMO01’s missing S07 sample already counted separately
as an "info"-severity finding above – is expected rather than a
bug: every record here was built with
build_ztem_record() and no
reference_station argument, so none of them carry the fixed
ground-reference metadata Technologies, Formats, and Native I/O already showed ZTEM’s
reference_required=True contract demands.
status is still only
"warning", not "error", because a missing reference station is
incomplete metadata, not an internally inconsistent one – the exact
severity philosophy Structural Quality Control explains in full, with a
dataset built to actually pass.