Skip to content
Of Ash and Fire Logo

SEG-Y Format: Byte Offsets, Headers and IBM Float

SEG-Y format reference: textual, binary and trace header byte offsets, sample format codes, revisions 0 to 2.1, and tested IBM float to IEEE conversion code.

TL;DR

Key Takeaways

  • A SEG-Y file is a 3200-byte textual header, a 400-byte binary header, optional 3200-byte extended textual headers, then traces made of a 240-byte trace header, any rev 2 extension headers, and samples.
  • Binary header bytes 3225 to 3226 hold the sample format code: 1 is 4-byte IBM floating point and 5 is 4-byte IEEE floating point; rev 2.0 added codes 6, 7, 9 to 12, 15 and 16.
  • The standard requires big-endian byte order for rev 0 and rev 1; rev 2 files declare their byte order with the constant 16909060 (0x01020304) in binary header bytes 3297 to 3300.
  • An IBM single equals (-1)^s × 0.F × 16^(e - 64); decoding to float64 is exact, while a float32 cast overflows above about 3.4 × 10^38 even though IBM singles reach about 7.2 × 10^75.
  • Rev 1 put inline and crossline numbers at trace header bytes 189 and 193, but older files vary: a seismic data specialist quoted by Agile Scientific had seen more than 180 different byte-location combinations.
·17 min read
oil and gasseg-yseismic dataibm floating pointfile formatslegacy integrationreverse engineering

The SEG-Y format is the Society of Exploration Geophysicists' standard for exchanging seismic data. A file starts with a 3200-byte textual header (EBCDIC or ASCII) and a 400-byte binary header, then optional 3200-byte extended headers, then traces: a 240-byte trace header plus samples. Format code 1 means IBM floats; code 5 means IEEE floats.

Reading the samples correctly is the hard part: byte order rules changed in 2017, format code 1 is an IBM System/360 float, and processing shops have moved trace header fields around since 1975. We wrote this reference while building our own SEG-Y parser and WebGL seismic viewer. Byte positions come from the SEG standards, and every code sample was run against known values. Files usually end in .sgy or .segy.

What are the SEG-Y revisions, and which one is my file?

SEG has published four revisions: rev 0 (1975), rev 1 (May 2002), rev 2.0 (January 2017) and rev 2.1 (October 2023). Binary header bytes 3501 and 3502 record the revision the writer claimed. Zero means rev 0.

RevisionPublishedWhat changed for a reader
Rev 01975, Geophysics vol. 40The 3200-, 400- and 240-byte headers and four sample formats, including IBM float. Big-endian. Trace header bytes 181 to 240 and binary header bytes 3261 to 3600 unassigned.
Rev 1May 2002IEEE float (code 5), 1-byte integers (code 8), extended textual headers, ASCII text, and standard slots for CDP X and Y (181 to 188), inline (189) and crossline (193). Still big-endian.
Rev 2.0January 2017Little-endian and pairwise byte-swapped files, IEEE double, 24-bit, 64-bit and unsigned integers, up to 65,535 extra trace headers per trace, 64-bit trace counts, data trailers.
Rev 2.1October 2023A survey type field at 3509 to 3510, carved out of the old 3507 to 3510 field, and deprecated appendices D-3 to D-5 removed.

Rev 1 stores the revision as one 16-bit word with the radix point between the bytes, so rev 1.0 is 0x0100. Rev 2 splits it into two 8-bit values, so rev 2.1 is 0x02 then 0x01. Read two single bytes and every revision decodes correctly, little-endian files included.

The rev 2.1 standard says SEG-Y was not designed for field recording and points to SEG-D or SEG-2, so raw shot records may arrive in those.

How is a SEG-Y file laid out on disk?

On disk, SEG-Y is a flat byte stream with no record markers. Bytes 1 to 3200 are the textual header (40 lines of 80 characters) and bytes 3201 to 3600 the binary header. Then come zero or more 3200-byte extended textual headers (count in bytes 3505 to 3506), the trace records back to back, and in rev 2 optional 3200-byte trailer records (count in bytes 3529 to 3532).

Rev 2 also defines an optional 128-byte tape label, and Appendix A warns that one may have been copied to the front of a disk file. That is the likely cause of a textual header shifted by 128 bytes.

The textual header is free-form. By convention each line starts with C (0xC3 in EBCDIC, 0x43 in ASCII), but not every file follows it, and extended header lines deliberately don't. Counting blanks is a sturdier test: the header is mostly spaces, 0x40 in EBCDIC and 0x20 in ASCII. Python's cp037 code page decodes the EBCDIC. Rev 2.1 asks for the revision on line 39 (SEG-Y_REV2.1) and END TEXTUAL HEADER on line 40. In old files these lines are often the only record of processing history, coordinate system and header locations.

SEG numbers bytes from 1, counting binary header bytes from the start of the file, so byte 3225 is offset 24 in the 400-byte block and trace byte 115 is offset 114. Off-by-one errors here still produce believable numbers.

Which binary header fields does a reader actually need?

A reader needs the format code (3225 to 3226), samples per trace (3221 to 3222), sample interval (3217 to 3218), byte order constant (3297 to 3300), revision (3501 to 3502), fixed-length flag (3503 to 3504) and extended header count (3505 to 3506). Rev 2 adds wider fields that override the 16-bit ones when nonzero; trust them only when the revision byte is 2 or higher, since older files may hold anything there.

BytesSizeFieldNotes
3217 to 32182Sample intervalµs for time, Hz for frequency, m or ft for depth
3221 to 32222Samples per data traceFor the primary traces in the file
3225 to 32262Data sample format codeSee the next section
3255 to 32562Measurement system1 = meters, 2 = feet
3269 to 32724Extended samples per traceRev 2; overrides 3221 to 3222
3273 to 32808Extended sample intervalRev 2; IEEE double; overrides 3217 to 3218. 3281 to 3292 hold the same pair for the original field recording.
3297 to 33004Byte order constantRev 2; 16909060 (0x01020304)
35011Major revisionRev 1 used 3501 to 3502 as one word
35021Minor revision0x01 for rev 2.1
3503 to 35042Fixed length trace flag1 = all traces the same length
3505 to 35062Extended textual header count-1 = variable, ends with ((SEG: EndText))
3507 to 35082Max additional trace headersRev 2.0 used 3507 to 3510
3513 to 35208Number of tracesUnsigned 64-bit
3521 to 35288Byte offset of first traceOverrides the offset implied by 3505 to 3506

The standard requires big-endian for rev 0 and rev 1. In rev 2, bytes 3297 to 3300 hold the integer 16909060. If a big-endian read returns 0x04030201, reverse the bytes of every binary header, trace header and sample field; if it returns 0x02010403, swap consecutive byte pairs. Zero tells you to assume the pre-rev 2 big-endian order. Textual headers and trailers are never swapped. Non-conforming writers exist, so cross-check with the format code: code 1 read with the wrong byte order comes out as 256, and code 5 as 1280.

What do the SEG-Y data sample format codes mean?

The two-byte code at binary header bytes 3225 to 3226 sets the type and width of every sample in the file. Code 1 is 4-byte IBM floating point, code 5 is 4-byte IEEE floating point, code 4 is obsolete, and the rest are integers or doubles.

CodeSample typeBytes
1IBM floating point4
2Two's complement integer4
3Two's complement integer2
4Fixed point with gain (obsolete)4
5IEEE floating point4
6IEEE floating point8
7Two's complement integer3
8Two's complement integer1
9Two's complement integer8
10Unsigned integer4
11Unsigned integer2
12Unsigned integer8
15Unsigned integer3
16Unsigned integer1

Rev 0 defined codes 1 to 4, rev 1 added 5 and 8, and rev 2.0 added the rest. Codes 13 and 14 are unassigned, and a 6 or 7 in a rev 1 file means damage or a private extension.

A trace record is the 240-byte header, 240 bytes per rev 2 extension header, then the samples. With the fixed-length flag at 1, trace k starts at the first-trace offset plus k record lengths. At 0 you walk the file, taking the sample count from trace bytes 115 to 116. If Trace Header Extension 1 is present, its bytes 137 to 140 override that count and bytes 157 to 158 give the number of extension blocks.

Expect code 1 in older archives. Matt Hall at Agile Scientific quoted a contact in 2021 who said 90% of the SEG-Y he saw in the wild still used IBM floats. That is one person's sample.

How do you convert IBM floating point to IEEE 754?

An IBM single is a sign bit, a 7-bit base-16 exponent biased by 64, and a 24-bit fraction with the radix point on its left, so the value is (-1)^s × 0.F × 16^(e - 64). Decode into float64, which holds every IBM single exactly, and cast to float32 afterward if you need to.

Take 0xC276A000. The sign bit is 1. The exponent bits are 0x42 (66), giving 16^2 = 256. The fraction is 0x76A000 / 2^24 = 0.46337890625. The product is 118.625, so the value is -118.625. That matches Wikipedia's worked example for IBM hexadecimal floating point and the layout in Appendix E of the rev 2.1 standard.

import numpy as np

def ibm_to_float(raw, byteorder=">"):
    """Decode 4-byte IBM System/360 floats (SEG-Y format code 1) to float64."""
    words = np.frombuffer(raw, dtype=np.dtype(np.uint32).newbyteorder(byteorder))
    sign = np.where(words >> 31, -1.0, 1.0)
    exponent = ((words >> 24) & 0x7F).astype(np.int64) - 64   # excess-64, base 16
    fraction = (words & 0x00FFFFFF).astype(np.float64) / 2**24  # 0.F, radix point on the left
    return sign * np.ldexp(fraction, 4 * exponent)               # 16**e == 2**(4e)

For a browser viewer reading an ArrayBuffer, the JavaScript version is shorter:

export function ibmToFloat(view, byteOffset, littleEndian = false) {
  const w = view.getUint32(byteOffset, littleEndian);
  const sign = w >>> 31 ? -1 : 1;
  const exponent = ((w >>> 24) & 0x7f) - 64;
  const fraction = (w & 0x00ffffff) / 0x1000000;
  return sign * fraction * Math.pow(16, exponent);
}

Both return -118.625 for 0xC276A000, 100.0 for 0x42640000, 1.0 for 0x41100000 and 0.00390625 for 0x3F100000. Each also matched an exact rational-arithmetic reference on 200,000 random 32-bit words, a test worth repeating on any faster bit-twiddling version. Other traps:

  • The IBM range is much wider than float32. The largest IBM single is about 7.2 × 10^75 and the smallest normalized one about 5.4 × 10^-79, while IEEE single precision tops out near 3.4 × 10^38. Cast to float32 and 0x7FFFFFFF becomes infinity; one corrupt sample like that ruins an auto-scaled color bar.
  • Precision wobbles. A base-16 exponent lets a normalized fraction start with up to three zero bits, leaving as few as 21 significant bits. IBM to float32 is exact inside float32's normal range, but IEEE back to IBM can lose up to three low-order bits.
  • The IBM format has no NaN or infinity. Every bit pattern is a number, so garbage decodes quietly.
  • Rev 1's Appendix E drew the fraction as 23 bits with the last bit forced to zero. Rev 2.1 shows 24. Decode all 24.
  • A wrong format code is hard to see in the numbers. Read 0x42640000 as IEEE and you get 57.0 instead of 100.0; read 0xC276A000 as IEEE and you get -61.65625 instead of -118.625.

Which trace header fields matter, and where do files actually put them?

For 3D post-stack data, the fields that matter are inline (189 to 192), crossline (193 to 196), CDP X and Y (181 to 188) with the coordinate scalar (71 to 72), samples per trace (115 to 116) and sample interval (117 to 118). Rev 0 left bytes 181 to 240 unassigned, so older files often keep inline and crossline numbers somewhere else.

Need Custom Software?

Get a free 30-minute architecture review with our team. 12+ years building enterprise applications.

BytesField
5 to 8Trace sequence number within file
21 to 24Ensemble number (CDP, CMP, CRP)
29 to 30Trace identification code (1 = time-domain seismic, 2 = dead)
71 to 72Scalar for coordinates in bytes 73 to 88 and 181 to 188
73 to 88Source X and Y, then group X and Y
89 to 90Coordinate units (1 = length, 2 = arc seconds, 3 = decimal degrees, 4 = DMS)
109 to 110Delay recording time
115 to 116Number of samples in this trace
117 to 118Sample interval for this trace
181 to 188CDP X and Y
189 to 192Inline number (3D post-stack)
193 to 196Crossline number (3D post-stack)
215 to 216Scalar for times in bytes 95 to 114

These positions are from Table 3 of the rev 2.1 standard. Real files depart from it in predictable places:

  • Inline and crossline move the most. In the same Agile Scientific post, Hall lists 189 and 193 as recommended, then "1 and 5 (well, 5 or 9). Or somewhere else." He reports that Don Robinson at Resolve had seen more than 180 byte-location combinations, and another company more than 300. Equinor's segyio defaults to 189 and 193 and accepts iline and xline arguments for files that differ.
  • Look for a layout before you guess. Rev 2 can embed an XML trace header layout as a ((SEG:Layout)) extended header stanza, and Appendix D-8 of rev 2.1 says the same XML can sit beside a rev 0 file to record where inline and crossline live. Failing that, read the textual header.
  • When you have to guess, test candidate fields in pairs. Skip the sequence counters at bytes 1 to 8, which step on every trace. In an inline-sorted volume, the crossline candidate should step regularly and reset each time the inline candidate increments, both ranges should match the survey grid, and the CDP X and Y values should move by one bin spacing per step.
  • Coordinates are integers with a scalar. Bytes 71 to 72 hold 1, ±10, ±100, ±1000 or ±10,000; positive multiplies, negative divides, zero means 1. Ignore a scalar of -100 and every coordinate comes out a hundred times too large. If bytes 89 to 90 say 2 or 3, X is longitude and Y is latitude.
  • A nonzero delay in bytes 109 to 110 shifts the trace in time, and it can be negative (allowed since rev 1). Apply the time scalar in bytes 215 to 216 before you use it.
  • The standard contradicts itself in one spot. Its binary header table and Table 4 put the extended sample count of Trace Header Extension 1 at bytes 137 to 140, while the Appendix A text says 129 to 132, which is actually the extended offset field. Use 137 to 140.

How do you read SEG-Y traces in Python?

If you only need the data, use a library such as segyio with the inline and crossline bytes you found. A reader of your own parses the file header, picks the byte order, finds the first trace, then walks trace records by fixed length or by each trace's sample count.

This code handles format codes 1 and 5, using ibm_to_float from above. It raises on pairwise-swapped files and on a variable extended header count, which both need a scan.

import struct
import numpy as np

BYTES_PER_SAMPLE = {1: 4, 2: 4, 3: 2, 4: 4, 5: 4, 6: 8, 7: 3, 8: 1,
                    9: 8, 10: 4, 11: 2, 12: 8, 15: 3, 16: 1}

def read_file_header(f, label=0):
    f.seek(label)                                   # label=128 if a tape label was copied in front
    text, binhdr = f.read(3200), f.read(400)
    # A heuristic, not a rule: textual headers are mostly blanks (0x40 EBCDIC, 0x20 ASCII)
    encoding = "cp037" if text.count(0x40) > text.count(0x20) else "ascii"
    rev = binhdr[300]                               # byte 3501 (rev 1 stores 0x0100 across 3501-3502)
    magic = binhdr[96:100]                          # bytes 3297-3300, unassigned before rev 2
    if rev < 2 or magic in (b"\x01\x02\x03\x04", b"\x00\x00\x00\x00"):
        bo = ">"
    elif magic == b"\x04\x03\x02\x01":
        bo = "<"
    else:
        raise ValueError(f"pairwise byte-swapped or unknown byte order: {magic.hex()}")
    get = lambda fmt, byte: struct.unpack_from(bo + fmt, binhdr, byte - 3201)[0]
    h = {"text": text.decode(encoding, errors="replace"), "byteorder": bo,
         "revision": (rev, binhdr[301]), "format": get("H", 3225),
         "sample_interval": get("H", 3217), "samples": get("H", 3221),
         "fixed_length": get("H", 3503) if rev >= 1 else 0,
         "n_ext_text": get("h", 3505) if rev >= 1 else 0,
         "extra_headers": 0, "n_traces": 0, "first_trace": 0}
    if rev >= 2:                                    # rev 2 fields override the 16-bit ones when nonzero
        h["samples"] = get("I", 3269) or h["samples"]
        h["sample_interval"] = get("d", 3273) or h["sample_interval"]
        h["extra_headers"] = get("H", 3507)
        h["n_traces"] = get("Q", 3513)
        h["first_trace"] = get("Q", 3521)           # counted from the start of the file
    if not h["first_trace"]:
        if h["n_ext_text"] < 0:
            raise ValueError("variable extended header count: scan for ((SEG: EndText))")
        h["first_trace"] = label + 3600 + 3200 * h["n_ext_text"]
    return h

def read_traces(f, h):
    """Yield (trace_header, samples) for format codes 1 and 5."""
    bo, width = h["byteorder"], BYTES_PER_SAMPLE[h["format"]]
    f.seek(h["first_trace"])
    count = 0
    while not h["n_traces"] or count < h["n_traces"]:
        th = f.read(240)
        if len(th) < 240:
            return
        n = struct.unpack_from(bo + "H", th, 114)[0]           # trace bytes 115-116
        if h["extra_headers"]:
            ext = f.read(240)
            n_extra = h["extra_headers"]
            if ext[232:240] in (b"SEG00001", "SEG00001".encode("cp037")):   # Extension 1
                n_extra = struct.unpack_from(bo + "H", ext, 156)[0] or n_extra   # bytes 157-158
                n = struct.unpack_from(bo + "I", ext, 136)[0] or n               # bytes 137-140
            f.seek(240 * (n_extra - 1), 1)
        if h["fixed_length"]:
            n = h["samples"]
        raw = f.read(n * width)
        if h["format"] == 1:
            yield th, ibm_to_float(raw, bo)
        elif h["format"] == 5:
            yield th, np.frombuffer(raw, dtype=bo + "f4")
        else:
            raise NotImplementedError(f"format code {h['format']}")
        count += 1

We tested it on synthetic files only: big- and little-endian rev 2.1 IBM files, a variable-length file with two extension headers per trace, a rev 1 file with junk where rev 2 fields now live, a rev 0 file, an EBCDIC header with blank-led lines, and a file behind a 128-byte label.

Should you convert SEG-Y to OpenVDS or ZGY for cloud and web viewing?

For an interactive working copy in the cloud or a browser, usually yes. VDS and ZGY store bricks, so slices in any direction read quickly; SEG-Y stores whole traces and reads fast only along its sort order. Keep the original SEG-Y as the archive of record.

A time slice from an inline-sorted volume needs a few bytes from every trace. dGB Earth Sciences, which added OpenVDS and OpenZGY support to OpendTect Pro 7, describes SEG-Y as fast inline, slower crossline and slower again for Z slices, and both brick formats as fast in all directions. A browser reading from object storage pays for that in many small range requests or in bytes it throws away.

OpenVDS comes from Bluware and is published through the OSDU program. Its documentation lists four compression options (none, run-length, Zip and wavelet), a default brick of 128 × 128 × 128 samples (64 cubed is recommended for uncompressed data), and levels of detail that halve each axis, up to LOD12. The open-source OpenVDS, licensed Apache 2.0, cannot create wavelet-compressed VDS; the free but closed-source OpenVDS+ can. With the original headers stored and lossless amplitude compression, VDS can export a bitwise-identical SEG-Y file; lossy compression gives that up. ZGY is SLB's Petrel format, available as OpenZGY through OSDU.

The SEG-Y visualization project decodes IBM floats in a custom parser and draws about 1.2 million points in about 80 ms with WebGL. For DepthSync we wrote a custom Rust OpenVDS decompressor to support real-time collaborative seismic editing. Our post on 3D seismic data visualization covers the rendering side.

Where should you start with a legacy SEG-Y archive?

Start with an inventory script that reads only file headers. Problems usually surface as disagreements between the binary header, the textual header and the file size.

  1. For every file, record the format code, byte order constant, revision bytes, extended header count, samples per trace, sample interval and file size.
  2. For fixed-length files, check that file size equals first-trace offset plus traces times record length, plus any trailer. A remainder points to a tape label, variable traces or a wrong sample count.
  3. Print the textual header and compare its claims about coordinates, units and header locations with the binary fields.
  4. Find where inline, crossline and coordinates actually live, and record the scalars.
  5. Decode a few traces as both IBM and IEEE and plot them; the 57.0 example shows why numbers alone can fool you.
  6. Write the findings to a manifest per file, leave the originals untouched, and convert a working copy to VDS or ZGY using the byte locations from the manifest.

Our guide to legacy system integration and reverse engineering applies the same spec-then-files method to other formats, and the posts on Wonderware and OSIsoft PI data extraction and refinery control system modernization cover plant data. Our oil and gas page lists related work. If a SEG-Y archive is blocking a cloud or web project, send us a few sample headers.

Founder & Lead Engineer at Of Ash and Fire. Building custom software for healthcare, education, and manufacturing.

Founder & Lead Developer at Of Ash and Fire · Test Double alumni · Former President, Techlahoma Foundation

Frequently Asked Questions

What is the SEG-Y file format?+
SEG-Y is the Society of Exploration Geophysicists' data exchange format for seismic data, first published in 1975. A file has a 3200-byte textual header, a 400-byte binary header, optional 3200-byte extended textual headers, and a series of traces. Each trace is a 240-byte trace header, plus any rev 2 extension headers, followed by samples in the format named by binary header bytes 3225 to 3226.
What does SEG-Y data sample format code 1 mean?+
Format code 1 means each sample is a 4-byte IBM System/360 floating point number: one sign bit, a 7-bit base-16 exponent biased by 64, and a 24-bit fraction. It dates from the 1975 rev 0 standard. Code 5, added in rev 1 in 2002, is 4-byte IEEE floating point, which most modern software expects.
How do I convert IBM float to IEEE float in Python?+
Read each sample as a 32-bit unsigned integer in the file's byte order (big-endian unless a rev 2 header says otherwise). Take the sign from bit 31, the exponent from bits 24 to 30 minus 64, and the fraction from the low 24 bits divided by 2^24. The value is sign × fraction × 16^exponent, and np.ldexp(fraction, 4 * exponent) computes it exactly in float64. 0xC276A000 should give -118.625.
How can I tell if a SEG-Y file is big-endian or little-endian?+
Check the revision at binary header byte 3501. The standard requires big-endian for rev 0 and rev 1. For rev 2 files, read bytes 3297 to 3300: 0x01020304 means big-endian, 0x04030201 means little-endian, 0x02010403 means byte pairs are swapped, and zero means assume big-endian. A format code that reads as 256 or 1280 points to the wrong byte order.
Where are inline and crossline numbers stored in a SEG-Y trace header?+
Rev 1 and later recommend trace header bytes 189 to 192 for the inline and 193 to 196 for the crossline in 3D post-stack data. Rev 0 left those bytes unassigned, so older files often use other locations such as bytes 1, 5 or 9. Check for a ((SEG:Layout)) XML stanza or sidecar, read the textual header, then confirm by scanning trace headers.
Should I convert SEG-Y to OpenVDS for cloud storage?+
For an interactive working copy, usually yes. OpenVDS and ZGY store data in bricks, so inline, crossline and time slices all read quickly, while SEG-Y is fast only along its sort order. With the original headers stored and lossless compression, VDS can export a bitwise-identical SEG-Y file, but keep the original SEG-Y as the archive of record anyway.

Ready to Ignite Your Digital Transformation?

Let's collaborate to create innovative software solutions that propel your business forward in the digital age.