PeakWright

PeakWright / Format reference

The ChemStation .ch and .uv formats

A working reference for the binary files inside an Agilent ChemStation .D folder. Written while building a reader for them, and checked against real files from six instruments.

Most of what follows is a restatement of the format descriptions published by the rainbow project, which is the source for the documented parts and deserves the credit for them.

Four things below are not in any published document. They were measured from real binaries. Those are marked, because the difference matters if you are implementing this.

What is in a .D folder

A ChemStation run is a directory, not a file. The two that hold signal data are:

Both start with a file type number that decides everything else about the layout. Read it first and branch on it. Guessing from the extension alone will not work, because the same extension covers layouts that share almost nothing.

File types

TypeExtHeaderBody encodingSeen from
30.ch0x400, one byte per characterDelta encoded int16/int32, big endianLegacy UV
130.ch0x1800, UTF-16Delta encoded int16/int32, big endianUV, and ADC for ELSD or CAD
179.ch0x1800, UTF-16float64, little endianOpenLab UV, FID
181.ch0x1800, UTF-16float64, little endianFID. Implemented, never verified: no public sample exists.
31.uv0x200, one byte per characterDelta encoded, little endianLegacy DAD
131.uv0x1000, UTF-16Delta encoded, or float64. Both occur.DAD

An unrecognised type should be refused by name rather than run through the closest matching path. A wrong chromatogram looks plausible, which makes it worse than an error message.

The delta encoding

The older bodies do not store values. They store changes, in segments.

Each segment begins with a count. Then that many signed 16-bit deltas follow, each added to a running accumulator. The special value -0x8000 is an escape: it is not a delta, it means the next 32 bits are a new absolute value for the accumulator. That is how the format handles a jump too large for 16 bits without widening every sample.

Byte order is the trap. .ch stores these big endian and .uv stores them little endian. Getting it wrong does not crash. It produces a chromatogram that looks like noise with roughly the right shape, which is easy to mistake for a bad run.

The accumulated integers are then multiplied by a scaling factor from the header to reach the plotted units.

Retention times

For .ch, the header carries the first and last time in milliseconds, at 0x11A and 0x11E, and the number of points. Times are reconstructed as an even ramp between them, because that is all the format provides.

That is worth stating plainly: a run recorded at a non-uniform sampling rate would be reported wrongly by any reader, including this one, because the information needed to do better is not in the file. Every sample checked so far is uniform.

For .uv, each retention time has its own 22-byte segment header carrying its own time, so the times are read rather than reconstructed.

Wavelengths in .uv

Wavelengths are stored as nanometres times 20, as integers. Divide by 20 to get nm. The segment header gives the first wavelength, the last, and the step, so the channel list is generated rather than stored per row.

The four offsets that are not documented

Everything in this section was measured from real binaries. It does not appear in any published format description, and the published values in two of these cases are wrong for the files actually in the wild.

1. Legacy types use a completely different string layout

Types 30 and 31 do not simply use a smaller version of the modern header. Their strings sit at entirely different offsets and use one byte per character rather than UTF-16. For example the acquisition date is at 0xB2 and the units string at 0x244, neither of which relates to where those live in a 130 or 131 file.

2. Legacy scaling factors

The scaling factor sits at 0x284 for .ch type 30, and at 0x13E for .uv type 31.

3. The .uv scaling factor is at 0xC0D, not 0x127C

The documented location for the scaling factor is 0x127C. For .uv that address is inside the data body, because the .uv header ends at 0x1000. The real value sits at 0xC0D, directly before the units string that the same documentation correctly places at 0xC15.

4. OpenLab writes .uv segments with label 70 and a plain float64 body

The documented .uv segment carries label 67 with the delta encoded body described above. Files written by OpenLab use label 70 and store plain little endian float64 values with no delta encoding at all. A reader that assumes label 67 will misread every OpenLab file. Both variants occur in the wild and both have to be handled.

Prior art

How this was checked

The reader is tested against 11 files from six instruments, comparing every decoded value to a reference CSV at a relative tolerance of 0.001. It currently matches on all of them with a maximum relative error of zero. A second test walks each reported byte region back through the decode independently, so that a claim about where a field lives has to hold up on its own.

You can check it against your own file without installing anything, at the viewer. The file is read in the browser tab and is not uploaded.

Citing this page

If this reference saved you time in a methods section or a repository, a citation is welcome.

PeakWright (2026). The Agilent ChemStation .ch and .uv binary formats. https://peakwright.com/format

Corrections

If you have a file that this gets wrong, that is the most useful thing you can send. Type 181 in particular is implemented but has never been checked against a real file, because no public sample exists. Write to [email protected], or open an issue on the parser repository.

PeakWright is not affiliated with or endorsed by Agilent Technologies. "ChemStation" and "OpenLab" are their trademarks.