Edit SEG-Y File Headers on MDIO¶
Brian Michell
Sep 22, 2026
7 min read
SEG-Y text and binary file headers live on the scalar segy_file_header variable as
textHeader and binaryHeader attributes. They are metadata only. Updating them does
not read or rewrite traces.
Ingest may skip these fields (MDIO__IMPORT__SAVE_SEGY_FILE_HEADER defaults to off).
Export back to SEG-Y needs both attributes. This tutorial shows how to add them when
they are missing, and how to replace or patch text and binary headers in place. Edits
range from one character to a completely new header; you always pass the payload you
want stored.
Warning
Update values of existing binary-header keys. Adding or removing keys can break
mdio_to_segy: the export factory only encodes fields from the SEG-Y spec, and it
requires sample_interval, samples_per_trace, and revision major/minor.
A tiny MDIO file without headers¶
Build a small in-memory dataset and write it with to_mdio. This is the same shape as a
file ingested with file-header saving turned off.
from pathlib import Path
import numpy as np
from mdio import open_mdio
from mdio import to_mdio
from mdio import update_segy_file_headers
from mdio.builder.template_registry import get_template
from mdio.builder.xarray_builder import to_xarray_dataset
mdio_path = Path("headers_demo.mdio")
template = get_template("PostStack2DTime")
template.full_chunk_shape = (4, 8)
mdio_ds = template.build_dataset(name="headers-demo", sizes=(4, 8))
dataset = to_xarray_dataset(mdio_ds).assign_coords(time=np.arange(8) * 4)
to_mdio(dataset, mdio_path, mode="w")
opened = open_mdio(mdio_path)
print("has segy_file_header:", "segy_file_header" in opened)
Add headers when they are missing¶
update_segy_file_headers creates segy_file_header if needed and fills defaults.
With no template, defaults follow SEG-Y Revision 1.0. samples_per_trace and
sample_interval come from the fastest (last) dimension of the default data
variable. Here that is 8 time samples at 4 ms, so the binary header stores
sample_interval=4000 microseconds. Zero or non-integer microsecond spacing
raises; there is no numeric fallback.
Pass template=get_segy_standard(2.0) (or any SegySpec) when the file should carry
that revision’s binary field set.
headers = update_segy_file_headers(mdio_path)
dataset = open_mdio(mdio_path)
print(dataset["segy_file_header"].attrs["textHeader"].split("\n")[0])
print("samples_per_trace", headers.binary_header["samples_per_trace"])
print("sample_interval", headers.binary_header["sample_interval"])
print("revision", headers.binary_header["segy_revision_major"], headers.binary_header["segy_revision_minor"])
To seed defaults from a different spec instead of Rev 1.0:
1update_segy_file_headers(mdio_path, template=get_segy_standard(2.0))
Replace or patch in place¶
Call the same function again. None arguments leave that header unchanged.
Text: whatever string you pass replaces
textHeader. Edit one character, one card, or supply a new 40x80 header — same call.Binary: keys you pass are merged onto the stored mapping. That is the safe way to change a value. A completely new binary header is a full mapping; do not drop keys the export spec needs.
Only textHeader and binaryHeader are written. Sibling attributes on
segy_file_header, including rawBinaryHeader, remain unchanged. Root attributes and
attributes on every other array also remain unchanged. For Zarr v2, the updater reads
individual node metadata rather than a possibly stale consolidated snapshot, then rebuilds
consolidated metadata after a successful update.
update_segy_file_headers(mdio_path, binary_header={"job_id": 42, "line_num": 7})
binary = open_mdio(mdio_path)["segy_file_header"].attrs["binaryHeader"]
print("job_id", binary["job_id"])
print("line_num", binary["line_num"])
print("samples_per_trace still", binary["samples_per_trace"])
Edit the text header in Python, then write it back¶
The stored value is one string (40 cards of 80 characters, joined by newlines). Change as little or as much as you want, then pass the whole string.
text = open_mdio(mdio_path)["segy_file_header"].attrs["textHeader"]
rows = text.split("\n")
# one character
rows[0] = rows[0][:4] + "X" + rows[0][5:]
# one card
rows[1] = "C02 LINE: DEMO-001".ljust(80)
# or assign a completely new 40-line header to `rows` / `text`
update_segy_file_headers(mdio_path, text_header="\n".join(rows))
updated = open_mdio(mdio_path)["segy_file_header"].attrs["textHeader"]
print(updated.split("\n")[0])
print(updated.split("\n")[1])
Binary headers: update values, do not reshape the field set¶
Safe:
1update_segy_file_headers(mdio_path, binary_header={"job_id": 99})
Risky:
Adding a key that is not in the SEG-Y spec used at export.
SegyFactoryraises when it cannot place the field.Removing a required key (
sample_interval,samples_per_trace,segy_revision_major,segy_revision_minor). Export then fails or writes a bad file.Switching revision by stuffing extra Rev 2 fields into a Rev 1 header (or the reverse) without passing a matching
templatethe next time defaults are built.
This helper never deletes keys: a partial binary_header is merged onto what is already
stored. To change revision layout, pass template when the file still lacks a binary
header, or replace the whole mapping only if you know the export SegySpec.
Recap¶
update_segy_file_headersadds or writestextHeader/binaryHeaderwithout touching traces.Missing fields get Rev 1.0 defaults, or a
SegySpecyou pass astemplate.sample_intervalandsamples_per_traceare inferred from the fastest dimension.Text: pass the full string, whether you changed one character or all 40 cards.
Binary: merge value updates. Do not add or remove keys unless the export spec matches.