15.3. EDI Interoperability#

EMTF.from_edi and EMTF.to_edi bridge the historical EDIFile model and the format-neutral EMTF document. The direction into EMTF is the one already exercised piecemeal throughout Metadata and The EMTF Document; this page assembles the full picture, then covers the return trip – which pycsamt treats as potentially lossy on purpose, since standard EDI genuinely cannot represent everything an EMTF document can hold. Everything on this page imports directly from the top level: from pycsamt.emtf import EMTF, EMTFEDIConversionError, DataLossWarning, write_edi, bundle_to_emtf, emtf_to_bundle.

15.3.1. What EDI Extraction Preserves#

Loading the real Gabbs Valley station (public-domain USGS data, https://doi.org/10.5066/P9GZ9Z56, already used throughout Metadata) populates four of EMTF’s seven metadata slots from one EDI file, in a single call – provenance, site, site_layout, and orientation; copyright, processing, and quality all stay None because this particular file has nothing explicit for any of them:

>>> from pathlib import Path
>>> from pycsamt.emtf import EMTF

>>> tf_gv = EMTF.from_edi(Path("data/gv_data/gv_final_edi/gv100.edi"))
>>> print(tf_gv.site.project, tf_gv.site.survey, tf_gv.site.name)
Energy Resources Program GV2020 Gabbs Valley
>>> print(tf_gv.processing)
None
>>> print(tf_gv.site_layout.input_names, tf_gv.site_layout.output_names)
('Hx', 'Hy') ('Hz', 'Ex', 'Ey')
>>> print(tf_gv.orientation.rotation_info)
EDI ZROT is constant at 347.5 deg relative to HEAD.COORDSYS='Geomagnetic North'; it was not promoted to an angle_to_geographic_north because historical EDI rotation metadata can be ambiguous.

tf_gv.processing is None for a reason already established in Processing and Quality: this particular file’s raw >INFO text has no structured processing keys at all, so there is nothing explicit to extract. site, site_layout, and orientation all come from structured EDI sections (HEAD/MTSECT/DEFINEMEAS/ZROT) that this file does populate. Every one of these extractions follows the same rule demonstrated repeatedly across Metadata: a field is set only when the corresponding EDI key is explicitly present, never inferred from context or defaulted from pycsamt’s own conventions.

15.3.2. Variance and the Legacy z_err Convention#

The EMTF Document showed that tf_gv.z_err is None even though a VAR estimate is attached, because pycsamt’s historical z_err field and the EMTF VAR estimate are related but not interchangeable. The adapter’s own docstring states the exact relationship: pycsamt has historically stored Z.z_err = sqrt(EDI complex variance), so edi_to_emtf reconstructs VAR as z_err ** 2 exactly – meaning np.sqrt(variance) (as done in The EMTF Document) recovers that legacy value precisely, not approximately. The per-component real or imaginary standard error, when that specific statistic is needed, is sqrt(VAR / 2) instead – a different, more refined quantity than the legacy z_err convention computes, and worth knowing before reaching for one when the other was actually wanted.

15.3.3. Converting Back to EDI#

A document that only ever held what the source EDI already expressed round-trips through to_edi() without any warning at all:

>>> import warnings

>>> with warnings.catch_warnings(record=True) as caught:
...     warnings.simplefilter("always")
...     edi_back = tf_gv.to_edi()
...     print(len(caught))
0

Attaching something EDI cannot hold – a citation, in this case, built the same way as in Provenance and Bibliography – makes the same call emit a DataLossWarning naming exactly what will not survive:

>>> from pycsamt.metadata import Reference, CopyrightInfo

>>> reference = Reference(
...     author="Peacock, J. R.; Siler, D. L.; Dean, B. J.; Zielinski, L. A.",
...     title="Magnetotelluric Data from the Gabbs Valley Region, Nevada, 2020",
...     year=2021, doi="10.5066/P9GZ9Z56",
... )
>>> tf_gv.copyright = CopyrightInfo(
...     release_status="Unrestricted Release",
...     conditions_of_use="See USGS terms.",
...     reference=reference,
... )
>>> with warnings.catch_warnings(record=True) as caught:
...     warnings.simplefilter("always")
...     edi_back2 = tf_gv.to_edi()
...     print(len(caught), caught[0].category.__name__)
...     print(caught[0].message)
1 DataLossWarning
current SEG EDI writer has no lossless mapping for EMTF Copyright/Citation metadata

on_loss controls what happens at that same point instead of warning: "raise" turns it into an EMTFEDIConversionError before anything is written, and "ignore" proceeds silently:

>>> from pycsamt.emtf import EMTFEDIConversionError

>>> try:
...     tf_gv.to_edi(on_loss="raise")
... except EMTFEDIConversionError as exc:
...     print(type(exc).__name__, exc)
EMTFEDIConversionError current SEG EDI writer has no lossless mapping for EMTF Copyright/Citation metadata

>>> with warnings.catch_warnings(record=True) as caught:
...     warnings.simplefilter("always")
...     tf_gv.to_edi(on_loss="ignore")
...     print(len(caught))
0

"ignore" is for a caller that has already reviewed what will be dropped and does not want the warning repeated on every call – not a default to reach for casually, since the dropped content is otherwise unrecoverable from the written EDI file.

15.3.4. When EDI Writing Refuses Outright#

Some conditions are not a loss-policy question at all: standard EDI has no impedance-shaped slot to omit gracefully, so writing a document with no impedance transfer function fails unconditionally, regardless of on_loss:

>>> empty_doc = EMTF()
>>> try:
...     empty_doc.to_edi()
... except EMTFEDIConversionError as exc:
...     print(type(exc).__name__, exc)
EMTFEDIConversionError standard EDI writing requires an impedance transfer function

The same unconditional check applies to shape and channel identity: the impedance matrix must be (n_period, 2, 2) with input channels exactly Hx/Hy and output channels exactly Ex/Ey (and tipper, when present, (n_period, 1, 2) with output Hz) – standard EDI’s Z/TIP blocks have no other layout to write into, so a document built around a different channel convention (the custom "admittance" type registered in Transfer Functions and Estimates, for example) cannot be written as EDI at all, loss policy or not.

15.3.5. Round-Trip Verification#

Writing and reloading a clean document (no attached copyright, which was cleared before this step) preserves every number exactly:

>>> import tempfile
>>> import numpy as np
>>> from pycsamt.emtf import write_edi

>>> tf_gv.copyright = None
>>> with tempfile.TemporaryDirectory() as tmp:
...     out_path = Path(tmp) / "gv100_roundtrip.edi"
...     written = write_edi(tf_gv, out_path)
...     tf_reloaded = EMTF.from_edi(written)
...     print(np.allclose(tf_gv.frequency, tf_reloaded.frequency))
...     print(np.allclose(tf_gv.z, tf_reloaded.z))
...     print(np.allclose(tf_gv.tipper, tf_reloaded.tipper, equal_nan=True))
...     print(tf_gv.site.name, "->", tf_reloaded.site.name)
True
True
True
Gabbs Valley -> GABBS VALLEY

The frequency grid, impedance, and tipper (NaN gaps included, via equal_nan=True) all compare exactly equal. site.name does not: the EDI writer upper-cases HEAD.LOC on write, so "Gabbs Valley" comes back as "GABBS VALLEY". This is a real, minor round-trip artifact – a text-casing convention of the EDI writer, not a scientific change – and a reminder that “numerically exact” and “byte-identical” are different, narrower claims than “unchanged.”

Converting through XML instead of back to EDI keeps everything, including the NaN gaps, with no analogous casing surprise (XML element text is not case-folded the way EDI HEAD fields are):

>>> with tempfile.TemporaryDirectory() as tmp:
...     xml_path = Path(tmp) / "gv100_roundtrip.xml"
...     tf_gv.write_xml(xml_path)
...     tf_from_xml = EMTF.from_xml(xml_path)
...     print(np.allclose(tf_gv.z, tf_from_xml.z))
...     print(np.allclose(tf_gv.tipper, tf_from_xml.tipper, equal_nan=True))
True
True

Reading and Writing EMTF XML covers the EMTFXMLReader/EMTFXMLWriter pair used above in full.

15.3.6. The TFBundle Bridge#

bundle_to_emtf() and emtf_to_bundle() are thin, explicitly typed wrappers around EMTF.from_bundle and EMTF.to_bundle, already covered in full – including exactly what survives and what does not – in The EMTF Document:

>>> from pycsamt.emtf import bundle_to_emtf, emtf_to_bundle

>>> bundle = emtf_to_bundle(tf_gv)
>>> back = bundle_to_emtf(bundle)
>>> print(type(bundle).__name__, type(back).__name__)
TFBundle EMTF
>>> print(np.allclose(back.z, tf_gv.z))
True

>>> try:
...     emtf_to_bundle("not-an-emtf")
... except TypeError as exc:
...     print(type(exc).__name__, exc)
TypeError document must be a pycsamt.emtf.EMTF

The only reason these wrappers exist alongside the methods is explicit type-checking at a stable function boundary – useful when the caller only has an arbitrary object and wants a clear error rather than an AttributeError several calls later.

15.3.7. Choosing the Right Function#

Need

Function

Notes

Load a document from EDI

EMTF.from_edi

Prefers EDI SPECTRA recovery when present – see EDI SPECTRA Recovery.

Write a document as EDI

EMTF.to_edi

Loss policy via on_loss; some failures are unconditional.

Write directly to a file path

write_edi()

Wraps to_edi plus the historical EDIFile.write.

Explicit TFBundle interop with a type check

bundle_to_emtf() / emtf_to_bundle()

See The EMTF Document for what the bundle bridge preserves.

15.3.8. Next Steps#