14.3. Channels and Orientation#
A transfer function is a matrix, but the matrix only means something
once its rows and columns are tied to physical channels.
ChannelMeta describes one measured channel – field type,
orientation angle, and sensor position – and SiteLayout
groups the full set of input and output channels that define a
station’s measurement geometry. OrientationMeta then records
how the data – the transfer-function matrix itself – relates to
geographic north, independently of that physical geometry. Both are
format-neutral metadata, mapping to EMTF XML’s
<SiteLayout> and <Orientation>/<RotationInfo> elements and
populated identically whether the source was EDI or XML.
Keeping site layout and orientation as two separate classes is
deliberate, not incidental. As OrientationMeta’s own
docstring states: it describes the orientation of the data, not the
physical sensor geometry, so that a transfer function can later be
rotated for processing while the physical channel layout it was
actually measured with stays exactly as recorded. Rotation is
scientific processing; SiteLayout is a historical fact about where
the sensors sat.
14.3.1. Reading a Real Site Layout#
data/gv_data/xml/gv100.xml – the real Gabbs Valley document
already used in Provenance and Bibliography – carries a
<SiteLayout> block with the standard MT convention: the two
horizontal magnetic channels as input, and the vertical magnetic plus
both electric dipoles as output.
Note
Public-domain USGS data; see Provenance and Bibliography for the required citation.
>>> from pycsamt.emtf import EMTF
>>> tf_gv = EMTF.from_xml("data/gv_data/xml/gv100.xml")
>>> layout = tf_gv.site_layout
>>> print(layout.input_names, layout.output_names)
('Hx', 'Hy') ('Hz', 'Ex', 'Ey')
>>> print(layout.n_input, layout.n_output)
2 3
>>> hx = layout.get_channel("Hx")
>>> print(hx)
ChannelMeta(name='Hx', field_type='magnetic', orientation=-12.5, tilt=None, x=0.0, y=0.0, z=0.0, x2=None, ...)
>>> ex = layout.get_channel("Ex", role="output")
>>> print(ex)
ChannelMeta(name='Ex', field_type='electric', orientation=347.459250694624, tilt=None, x=0.0, y=0.0, z=0.0, x2=50.8, ...)
>>> print(ex.is_electric, ex.has_second_endpoint)
True True
>>> print(hx.has_second_endpoint)
False
get_channel() searches both groups by default;
role="output" above restricts the search when a name could exist
on either side (it cannot here, but a symmetric processing scheme
might reuse a name). has_second_endpoint is
True for Ex because an electric dipole has two electrodes –
the real x2=50.8 above is the second electrode’s position 50.8 m
from the first along the local x axis – while a magnetic coil is
a point sensor and never carries x2/y2/z2.
Not every field the real station recorded made it into
ChannelMeta, and that is also by design. The vertical channel’s
free-text field notes elsewhere in the same document say
gv100a.hz.measurement_tilt = 90.0 – physically correct, a
vertical coil – but the structured <SiteLayout> element’s
<Magnetic name="Hz" .../> tag carries no tilt attribute at
all, so ChannelMeta leaves it unset instead of inferring 90° from
the channel’s name:
>>> hz = layout.get_channel("Hz")
>>> print(hz.tilt)
None
14.3.2. Field Types and Manual Construction#
field_type accepts common aliases – "e"/"electric",
"h"/"b"/"magnetic", "other" – and normalizes them to
one of three canonical values at construction:
>>> from pycsamt.metadata import ChannelMeta, SiteLayout
>>> manual = ChannelMeta(name="Hx", field_type="h", orientation=-12.5, x=0.0, y=0.0, z=0.0)
>>> print(manual.field_type)
magnetic
>>> try:
... ChannelMeta(name="Bad", field_type="unknown")
... except ValueError as exc:
... print(type(exc).__name__, exc)
ValueError field_type must describe electric, magnetic, or other data
SiteLayout rejects a channel name that collides
case-insensitively with another channel in the same group – the
same identity discipline as Station identity applied one level
down, at the channel rather than the station:
>>> try:
... SiteLayout(
... input_channels=[
... ChannelMeta(name="Hx", field_type="h"),
... ChannelMeta(name="hx", field_type="h"),
... ]
... )
... except ValueError as exc:
... print(type(exc).__name__, exc)
ValueError duplicate channel name in input_channels
14.3.3. Two Real Layouts Compared#
The WILLY L18 EDI line used throughout Site and Survey Metadata has the same input/output structure as the Gabbs Valley XML document, despite coming from an unrelated survey and a different processing chain:
>>> from pathlib import Path
>>> from pycsamt.seg.edi import EDIFile
>>> edi = EDIFile(Path("data/AMT/WILLY_DATA/L18PLT/18-001A.edi"))
>>> tf_willy = EMTF.from_edi(edi)
>>> print(tf_willy.site_layout.input_names, tf_willy.site_layout.output_names)
('Hx', 'Hy') ('Hz', 'Ex', 'Ey')
The figure below plots each station’s horizontal channel orientations on a compass, magnetic channels solid/dashed in blue and electric channels solid/dashed in red at a shorter radius so a near-identical direction does not hide one arrow behind the other:
import numpy as np
import matplotlib.pyplot as plt
def channel_angles(layout):
out = {}
for group in (layout.input_channels, layout.output_channels):
for ch in group:
if ch.name.lower() != "hz" and ch.orientation is not None:
out[ch.name] = ch.orientation
return out
gv_angles = channel_angles(tf_gv.site_layout)
willy_angles = channel_angles(tf_willy.site_layout)
radius = {"Hx": 1.0, "Hy": 1.0, "Ex": 0.62, "Ey": 0.62}
colors = {"Hx": "#1f77b4", "Hy": "#1f77b4", "Ex": "#d62728", "Ey": "#d62728"}
styles = {"Hx": "-", "Ex": "-", "Hy": "--", "Ey": "--"}
fig, axes = plt.subplots(1, 2, figsize=(9, 5), subplot_kw={"projection": "polar"})
for ax, angles, title in zip(
axes,
(gv_angles, willy_angles),
("gv100 (Gabbs Valley, XML)", "18-001A (WILLY L18, EDI)"),
):
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
for name, angle in angles.items():
theta = np.deg2rad(angle)
r = radius[name]
ax.annotate(
"", xy=(theta, r), xytext=(0, 0),
arrowprops=dict(arrowstyle="-|>", color=colors[name], lw=2, ls=styles[name]),
)
ax.text(theta, r + 0.18, f"{name} {angle:.1f}°", ha="center", va="center",
fontsize=8, color=colors[name])
ax.set_ylim(0, 1.4)
ax.set_yticklabels([])
ax.set_title(title, fontsize=10, pad=24)
fig.tight_layout()
Horizontal channel orientations for both real stations. In each
case Ex points in almost exactly the same direction as Hx,
and Ey almost exactly matches Hy – the electric dipoles
were laid out along the same reference axes as the magnetic coils.#
The two stations differ in how exact that agreement is. WILLY’s
Hx/Ex are both precisely \(0^\circ\) and
Hy/Ey both precisely \(90^\circ\) – a nominal orthogonal
layout with no measured deviation recorded. Gabbs Valley’s Hx sits
at \(-12.5^\circ\) while Ex sits at
\(347.459250694624^\circ \equiv -12.540749^\circ\), a real
\(0.04^\circ\) difference consistent with as-built electrode
placement not landing on the coil azimuth exactly – the kind of
small, genuine field tolerance a nominal-only layout would hide.
14.3.4. Orientation Mode and Rotation History#
Neither real document declares an explicit orientation mode:
>>> print(tf_gv.orientation)
OrientationMeta(mode=None, angle_to_geographic_north=None, rotation_info='EDI ZROT is constant at 347.5 deg relative to...', extra=dict(len=0, keys=[]))
>>> print(tf_gv.orientation.is_orthogonal, tf_gv.orientation.follows_site_layout)
False False
>>> print(tf_willy.orientation)
OrientationMeta(mode=None, angle_to_geographic_north=None, rotation_info='EDI ZROT is constant at 0 deg relative to HEA...', extra=dict(len=5, keys=['edi_coordsys', 'edi_had_zrot', 'edi_had_trot', ...]))
Both carry a human-readable rotation_info instead: the EDI
adapter recognized a constant ZROT in each file but, because
neither file’s HEAD.COORDSYS says "geographic" (Gabbs Valley
says "Geomagnetic North"), it declined to promote that angle into
angle_to_geographic_north – exactly the pattern already seen in
Provenance and Bibliography. Since mode is None rather
than either recognized value, both convenience properties correctly
report False for both stations: pycsamt does not guess that an
undeclared mode means “probably orthogonal.”
Constructing OrientationMeta directly shows the accepted modes
and the one constraint tying them to angle_to_geographic_north:
>>> from pycsamt.metadata import OrientationMeta
>>> layout_mode = OrientationMeta(mode="site_layout")
>>> print(layout_mode.mode, layout_mode.follows_site_layout)
sitelayout True
>>> orthogonal_mode = OrientationMeta(mode="orthogonal", angle_to_geographic_north=12.3)
>>> print(orthogonal_mode.mode, orthogonal_mode.is_orthogonal, orthogonal_mode.angle_to_geographic_north)
orthogonal True 12.3
>>> try:
... OrientationMeta(mode="sitelayout", angle_to_geographic_north=10.0)
... except ValueError as exc:
... print(type(exc).__name__, exc)
ValueError sitelayout orientation is defined by channel geometry; angle_to_geographic_north must be None
"site_layout"/"site-layout"/"layout" are accepted spellings
that all normalize to "sitelayout". The last error is a real
consistency check, not pedantry: "sitelayout" mode means the
transfer function is still expressed in the physical channel frame
recorded by SiteLayout, so an additional rotation angle to
geographic north would describe two contradictory things at once.
14.3.5. Choosing the Right Object#
Need |
Object |
Notes |
|---|---|---|
One channel’s field type, orientation, and geometry |
|
|
The full set of channels a matrix’s axes are defined against |
Rejects duplicate names within a group; look up with |
|
How the transfer-function matrix relates to geographic north |
Independent of physical geometry; |
14.3.6. Next Steps#
Site and Survey Metadata covers station identity and campaign-level aggregation;
Provenance and Bibliography covers who created and processed a document and how to cite it;
Instrument Metadata covers the acquisition system that recorded these channels.