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. SegyFactory raises 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 template the 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_headers adds or writes textHeader / binaryHeader without touching traces.

  • Missing fields get Rev 1.0 defaults, or a SegySpec you pass as template. sample_interval and samples_per_trace are 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.