---
title: "HEAEV Guide"
subtitle: "Version 0.1"
author: "Keith A. Arnaud"
date: "2026-04"
---

# HEAEV Guide

**Version 0.1** -- Keith A. Arnaud.  April 2026.

---

# Introduction

HEAEV is a C++ library for reading FITS event files, applying
selection filters per event, and accumulating output products such as
images, spectra, lightcurves, and event lists. It is the
mission-agnostic engine behind the `ftextractor` task and is intended
as the building-block library for any future heasoft tool that walks
an event file and produces filtered or histogrammed outputs.

The design separates three concerns:

- **Metadata.** Per-file header information --- WCS, time system,
  column identities, data subspace --- is read into an `eventInfo`
  object and merged across multiple input files via a documented A/B/C
  taxonomy of "must match", "combine by rule", and "take from first".
- **Filtering.** Each selection criterion (good-time interval, region,
  grade, key-range, data-subspace, phase) is encapsulated in its own
  `filter` subclass. A `filterChain` ANDs them in a fixed order and
  attributes each rejected event to the first filter that flipped its
  mask bit, so per-filter rejection counts never double-count.
- **Accumulation.** Each output product is an `eventAccumulator`
  subclass that consumes `(block, mask)` pairs from the reader and
  maintains its own state. Concrete accumulators exist for images,
  spectra, lightcurves, WMAPs, Stokes parameters, unbinned text dumps,
  and the row-index list used by event-output writers. Output FITS or
  text formats are *not* the accumulator's job; the driver program
  reads accumulator state and writes whatever format it wants.

HEAEV does not know about specific missions. Mission-specific
behaviour enters via the `.par` parameters of the calling task and via
the input file's own header keywords. There is no instrument or
telescope branching anywhere in the library.

There are no Python bindings yet. There is no C interface. The library
is C++11 and is consumed through its headers. The DESIGN.md document
in the `ftextractor` source directory captures the architectural
rationale and the boundary rule between heaev and its callers.

This guide is organised in three layers: a Pipeline Overview chapter
that walks through both the library and a worked end-to-end example;
a Class Reference chapter that documents every public symbol; and
short chapters on error handling, building, and known clients.

The guide assumes familiarity with C++ at the level needed to use
heasp. Where heaev relies on heasp types (`heasp::Real`,
`heasp::Integer`, `heasp::OK`) or error codes, this guide
cross-references the heasp guide rather than duplicating its
chapters.

## Highlights

- Block-oriented per-file iteration. The `eventReader` reads rows in
  CFITSIO-recommended stride-sized blocks; filters and accumulators
  operate on the block at a time, not per event.

- Composable filter chain. Six concrete filters --- `gtiFilter`,
  `phaseFilter`, `gradeFilter`, `keyvalFilter`, `dsFilter`,
  `regionFilter` --- can be assembled in any combination. The chain
  AND-folds them and attributes rejections to the first filter that
  flipped each bit.

- Eight concrete accumulators covering images (with optional Z
  weighting), spectra, lightcurves with TIMEPIXR correction, WMAPs,
  the WMAP/image relation moments needed by `fixwmp`, Stokes
  parameters, unbinned text dumps, and a disk-spilled row-index list.

- Multi-file aware. `eventInfo::combine` merges per-file headers using
  the A/B/C policy. The reader carries a per-file index on every
  block so accumulators that need to know provenance (e.g. the
  `rowIndexAccumulator`) can record it.

- WCS routines that match what extractor uses --- CFITSIO's `ffwldp`
  and `ffxypx`, accessible from `imageInfo::pixelToSky`,
  `skyToPixel`, and the per-file-frame `transformTo`.

- A small DS-keyword toolkit. `dataSubspace` reads DSTYPi/DSVALi
  keywords, compiles them into a fast `dsFilter` predicate, set-unions
  chip-column values across files, and prunes keys for chips that
  contributed no events.

## Querying the version at runtime

```cpp
#include "heaev_version.h"
#include <iostream>

std::cout << "heaev " << heaev::heaevVersion << "\n";
```

The constant lives in `heaev_version.h` and is bumped whenever the
change-log chapter at the end of this guide gets a new entry.

---

# Pipeline overview

A heaev tool typically does five things:

1. **Read header metadata** for one or more input event files into
   `eventInfo` objects, combining them into a single merged
   `eventInfo` if there is more than one input.
2. **Build a `filterChain`** with the filters that match the user's
   selection. Filters that the user did not request are simply not
   added; the chain is empty by default.
3. **Build a vector of accumulators**, one per output product. The
   chain and the accumulator vector together determine which event
   columns the reader will materialise.
4. **Iterate**, one input file at a time, by constructing an
   `eventReader` and calling its `iterate` method. The same chain and
   accumulator vector are passed in each call.
5. **Drain accumulator state into output files.** The accumulators
   own no FITS knowledge; the driver reads their `image()`,
   `spectrum()`, `counts()`, etc. accessors and writes whatever
   format it wants, often via heasp.

The two examples below show this five-step flow at two levels of
detail. The column names match the test file
`ae405028010xi0_0_3x3n066l_cl.evt`, a
small Suzaku XIS cleaned event file used in heaev's parity tests.
Substitute your own column names; heaev itself does not know about
Suzaku.

## Example 1: hello world

The minimum example. One input file, one filter (good-time intervals
already declared in the file), one accumulator (image). Prints the
total count and pixels hit.

```cpp
#include <iostream>

#include "eventInfo.h"
#include "eventReader.h"
#include "filterChain.h"
#include "gtiFilter.h"
#include "imageAccumulator.h"

int main() {
  const std::string infile =
    "ae405028010xi0_0_3x3n066l_cl.evt";

  // 1. Read header metadata.
  eventInfo info;
  if (info.readFromOneFile(infile,
                           "DETX", "DETY",   // image cols
                           "DETX", "DETY",   // wmap cols (unused here)
                           "TIME", "PI",     // time and energy cols
                           "CCD_ID", "",     // chip col, no grade
                           "EVENTS",
                           "", "", "", "", "") != 0) {
    std::cerr << "header read failed\n"; return 1;
  }

  // 2. Build the filter chain. The GTIs have been read into
  //    info.getGtis(); pass them to a gtiFilter.
  filterChain chain;
  chain.add(std::unique_ptr<filter>(
      new gtiFilter(info.getGtis(), "TIME", "CCD_ID",
                    info.getReferenceDay(),
                    info.getReferenceTime() * 86400.0)));

  // 3. Build the accumulator vector. The image bounding box comes
  //    from the file's TLMIN/TLMAX for X and Y.
  const imageInfo& img = info.getImage();
  imageAccumulator imgAcc("DETX", "DETY",
                          (int)img.getXMin(), (int)img.getYMin(),
                          (int)img.getXMax(), (int)img.getYMax(),
                          /*binFactor=*/1);
  std::vector<eventAccumulator*> accs = { &imgAcc };

  // 4. Iterate.
  eventReader reader(infile, "EVENTS");
  if (!reader.good()) { std::cerr << "open failed\n"; return 1; }
  long n = reader.iterate(chain, accs);
  if (n < 0) { std::cerr << "read error\n"; return 1; }

  // 5. Report.
  long counts = 0;
  for (const auto& row : imgAcc.image())
    for (double v : row) counts += (long)v;
  std::cout << n << " events processed, "
            << counts << " in image, "
            << imgAcc.pixelsHit() << " pixels hit\n";
  return 0;
}
```

This compiles with the same link line as `ftextractor` (see the
*Building programs using heaev* chapter): `-lheaev -lheasp -lCCfits
-lcfitsio -lhdsp_6.36 -lhdutils_6.36 -lhdio_6.36 -lhdinit_6.36`.

A few details that are easy to miss in a 30-line example:

- The `gtiFilter` constructor takes the events' MJDREF in
  `(int days, double seconds-into-day)` form. `eventInfo` stores
  the second component as a fraction-of-day, so the multiplication
  by 86400.0 gets it into seconds-since-midnight, which is what
  `gtiFilter` (and `GTI` internally) wants. This conversion is the
  single rough edge of the heaev MJDREF API and a likely target for
  cleanup in a later version.

- The `imageAccumulator` bounding box is supplied as ints; the file's
  TLMIN/TLMAX come back as doubles from `imageInfo`. The cast is
  intentional: bin coordinates are always integer.

- `reader.iterate` returns the number of rows processed (not the
  number that passed the filter). The filter chain's pass mask is
  hidden inside the iteration; if you want per-filter rejection
  counts, ask the chain via `chain.rejectionCounts()` after the
  iteration completes.

## Example 2: two files, two filters, two accumulators, PHA output

The first example does not show how heaev composes; this one does.
Two input files (both the same Suzaku exposure for simplicity) get
their headers combined; a `keyvalFilter` selecting events with PI in
`[200, 1000]` is added on top of the GTI filter; a spectrum is
accumulated alongside the image; the spectrum is written out as a
heasp PHA file.

```cpp
#include <iostream>
#include <map>

#include "eventInfo.h"
#include "eventReader.h"
#include "filterChain.h"
#include "gtiFilter.h"
#include "keyvalFilter.h"
#include "imageAccumulator.h"
#include "spectrumAccumulator.h"

#include "pha.h"        // heasp

int main() {
  const std::vector<std::string> infiles = {
    "ae405028010xi0_0_3x3n066l_cl.evt",
    "ae405028010xi0_0_3x3n066l_cl.evt"
  };

  // 1. Read each file's header, combine into a single eventInfo.
  eventInfo combined;
  for (size_t i = 0; i < infiles.size(); ++i) {
    eventInfo ei;
    if (ei.readFromOneFile(infiles[i],
                           "DETX", "DETY", "DETX", "DETY",
                           "TIME", "PI", "CCD_ID", "",
                           "EVENTS", "", "", "", "", "") != 0) {
      std::cerr << "header read failed for " << infiles[i] << "\n";
      return 1;
    }
    if (i == 0) combined = ei;
    else {
      auto r = combined.combine(ei);
      if (!r.ok) {
        for (const auto& m : r.mismatches) std::cerr << m << "\n";
        return 1;
      }
    }
  }

  // 2. Build the chain: GTI + keyval (PI in [200, 1000]).
  filterChain chain;
  chain.add(std::unique_ptr<filter>(
      new gtiFilter(combined.getGtis(), "TIME", "CCD_ID",
                    combined.getReferenceDay(),
                    combined.getReferenceTime() * 86400.0)));

  std::unique_ptr<keyvalFilter> kf(new keyvalFilter);
  kf->addRange("PI", 200.0, 1000.0);
  chain.add(std::unique_ptr<filter>(kf.release()));

  // 3. Build accumulators: image + spectrum.
  const imageInfo& img = combined.getImage();
  imageAccumulator imgAcc("DETX", "DETY",
                          (int)img.getXMin(), (int)img.getYMin(),
                          (int)img.getXMax(), (int)img.getYMax());
  spectrumAccumulator spec("PI",
                           (int)combined.getEnergyColMin(),
                           (int)combined.getEnergyColMax(),
                           /*specBin=*/1);
  std::vector<eventAccumulator*> accs = { &imgAcc, &spec };

  // 4. Iterate over each input file with the same chain.
  long totalProcessed = 0;
  for (size_t i = 0; i < infiles.size(); ++i) {
    eventReader reader(infiles[i], "EVENTS");
    if (!reader.good()) {
      std::cerr << "open failed for " << infiles[i] << "\n";
      return 1;
    }
    reader.setFileIndex(i);
    long n = reader.iterate(chain, accs);
    if (n < 0) return 1;
    totalProcessed += n;
  }

  // 5a. Report counts and rejection breakdown.
  long imgCounts = 0;
  for (const auto& row : imgAcc.image())
    for (double v : row) imgCounts += (long)v;
  std::cout << totalProcessed << " events processed across "
            << infiles.size() << " files\n";
  std::cout << "image: " << imgCounts << " counts\n";
  for (const auto& kv : chain.rejectionCounts())
    std::cout << "  rejected by " << kv.first << ": "
              << kv.second << "\n";

  // 5b. Drain the spectrum into a heasp::pha and write it.
  using namespace heasp;
  pha out;
  RealVector phaCounts(spec.spectrum().begin(), spec.spectrum().end());
  RealVector zeros(phaCounts.size(), 0.0);
  IntegerVector channels(phaCounts.size());
  for (size_t k = 0; k < channels.size(); ++k)
    channels[k] = spec.firstChannel() + (int)k * spec.specBin();
  IntegerVector quality(phaCounts.size(), 0);
  IntegerVector grouping(phaCounts.size(), 1);
  RealVector areaScal(phaCounts.size(), 1.0);
  RealVector backScal(phaCounts.size(), 1.0);
  std::map<std::string, std::string> keys;
  keys["TELESCOP"] = combined.getTelescope();
  keys["INSTRUME"] = combined.getInstrument();

  Integer status = out.load(phaCounts, zeros, zeros,
                            channels, quality, grouping,
                            areaScal, backScal,
                            spec.firstChannel(),
                            /*exposure=*/0.0,
                            /*correctionScaling=*/1.0,
                            /*detChans=*/(int)phaCounts.size(),
                            /*poisserr=*/true,
                            keys);
  if (status != heasp::OK) { std::cerr << "pha load failed\n"; return 1; }
  status = out.write("example2.pha");
  if (status != heasp::OK) { std::cerr << "pha write failed\n"; return 1; }
  return 0;
}
```

The points this example illustrates that example 1 does not:

- **`eventInfo::combine`.** The merge returns a `combineResult`; on
  any Class A mismatch the call sets `ok=false` and lists every
  mismatched field in `mismatches` rather than aborting at the first
  one. The driver decides what to do with the list. (The test files
  used here are identical, so the mismatch list is always empty.)

- **`keyvalFilter::addRange`.** Adds one allowed range for one key.
  Multiple `addRange` calls with the same key OR together; calls with
  different keys AND together. The legacy `[KEY=lo:hi,...]` syntax
  in the input filename is parsed into the same shape via the
  separate `eventFilenameParse` helper used by `ftextractor`.

- **`chain.rejectionCounts()`.** Each filter contributes one entry to
  the map, keyed by `filter::name()`. An event is attributed to the
  *first* filter in the chain that flipped its mask bit, so the
  counts add up to (rows processed − good events) without double
  counting.

- **Per-file iteration with `setFileIndex`.** The same chain and
  accumulator vector are reused across files. The reader stamps each
  block with its file index so accumulators that care about
  provenance can record it. (Only the `rowIndexAccumulator` does; the
  rest ignore `fileIndex`.)

- **Bridging to heasp::pha.** The `spectrumAccumulator` exposes its
  bin counts, first-channel offset, and specBin via plain accessors;
  the rest is a routine `pha::load` plus `pha::write`. heaev does not
  know about PHA file format or any other on-disk format.

The full ftextractor source (`heasptools/ftextractor/ftextractor.cxx`)
shows what scaling this pattern up to all six filters and all eight
accumulators looks like, plus FITS event-output via the
`rowIndexAccumulator` temp-file route. Read it as the long-form
worked example for this guide.

## Filter chain ordering

The chain runs filters in the order they were `add`ed. This is the
order ftextractor uses, and the order chosen so that per-filter
rejection counts mirror the Fortran extractor's table:

1. `gradeFilter`
2. `gtiFilter`
3. `phaseFilter`
4. `keyvalFilter`
5. `dsFilter`
6. `regionAdapter` (wrapping `regionFilter`)

Reordering changes the per-filter rejection counts (because each
event is attributed to the first filter that rejects it) but does
not change which events pass the chain (the chain is a pure AND).
Drivers free to deviate from this order if they have reason to;
ftextractor documents its choice in DESIGN.md.

## Column materialisation

The reader does not read every column of the events extension. It
reads only the columns that the active chain or accumulators
declare via `requiredColumns()`. This matters more than it sounds:
event extensions for some missions carry dozens of columns, and
materialising columns the tool will not use is wasted I/O.

The set is computed once before iteration begins:

```cpp
std::set<std::string> needed = chain.requiredColumns();
for (auto* a : accumulators) {
  auto cols = a->requiredColumns();
  needed.insert(cols.begin(), cols.end());
}
```

If you write a custom filter or accumulator, populate
`requiredColumns()` correctly. A column that appears in `compute` or
`update` but is missing from `requiredColumns()` will throw
`std::out_of_range` at the first block.

---

# Class reference

This chapter is the canonical reference for every public symbol in
heaev. The standalone classes (`eventBlock`, `GTI`, `GTIs`,
`imageInfo`, `eventInfo`, `dataSubspace`, `filterChain`,
`eventReader`) get full sections with private members documented. The
filter and accumulator hierarchies have abstract-base sections
followed by per-concrete subsections that document only the
configuration surface (constructor, setters) and the result accessors
that consumers read. Private members of concrete filter/accumulator
subclasses are intentionally undocumented; they are implementation
detail.

Methods are documented as definition lists. Each list entry is the
method's signature followed by an indented description.

## eventBlock

A block of events read from one input file. Filters operate on a
block at a time; accumulators consume `(block, mask)` pairs.

### eventBlock public members

A `struct` with public fields:

`size_t fileIndex`
:   Identifier of the input event file this block came from. Set by
    the reader from `eventReader::fileIndex()`. Defaults to 0; only
    meaningful across a multi-file iteration.

`long firstRow`
:   1-based row number of the first event in the block, in CFITSIO
    convention.

`size_t size`
:   Number of events in the block. Equals the size of every
    materialised column.

`std::map<std::string, std::vector<double>> columns`
:   Column data, keyed by column name. Populated only with the
    columns required by the active chain and accumulators. All
    column values are stored as `double` for uniformity, even integer
    columns. Consumers needing an integer typically do
    `(int)round(v)`.

### eventBlock public methods

`const std::vector<double>& column(const std::string& name) const`
:   Accessor for a column. Throws `std::out_of_range` if the column
    is not present in `columns`.

`bool hasColumn(const std::string& name) const`
:   Returns true if a column with the given name has been
    materialised in this block.

## GTI

A single good-time interval table: a vector of `(start, stop)` pairs
plus an MJDREF expressed as a `(days, seconds)` pair. Times are in
seconds since the MJDREF.

### GTI private members

| Type | Member | Description |
|------|--------|-------------|
| `std::vector<std::pair<heasp::Real,heasp::Real>>` | `m_Intervals` | Start/stop pairs in seconds |
| `heasp::Integer` | `m_MJDREF_days` | Integer days component of MJDREF |
| `heasp::Real` | `m_MJDREF_seconds` | Seconds-into-day component of MJDREF |

### GTI public methods

`GTI()`
:   Constructor. `m_Intervals` is empty, MJDREF is `(0, 0.0)`. The
    `(0, 0)` pair is treated as a sentinel meaning "no MJDREF
    declared"; methods that translate by MJDREF (`merge`,
    `changeMJDREF`) will not translate intervals against this
    sentinel without explicit relabelling.

`~GTI()`
:   Destructor; nothing special.

`GTI& operator=(const GTI&)`
:   Deep copy assignment.

`void clear()`
:   Empties `m_Intervals` and resets MJDREF to `(0, 0.0)`.

`bool isInGTI(heasp::Real time, heasp::Integer mjdrefi, heasp::Real mjdrefr) const`
:   Returns true if a single time, expressed in the MJDREF
    `(mjdrefi, mjdrefr)` frame, falls inside any interval. The
    function translates the supplied time into the GTI's own MJDREF
    frame before testing.

`std::vector<bool> isInGTI(std::vector<heasp::Real> time, heasp::Integer mjdrefi, heasp::Real mjdrefr) const`
:   Vector overload of the above. Returns one `bool` per input time.

`void order()`
:   Sorts intervals by start time and merges overlapping intervals
    in place. Idempotent.

`std::string disp() const`
:   Returns a multi-line human-readable dump of the intervals and
    MJDREF, suitable for diagnostics.

`size_t size() const`
:   Number of intervals.

`void merge(const GTI& other, bool andmode)`
:   Merge another GTI into this one. If `andmode=true`, the result is
    the intersection (logical AND) of the two interval sets; if
    false, the union (logical OR). Both inputs are assumed already
    sorted (`order()`-ed). Intervals from `other` are translated by
    the MJDREF difference before merging if the two MJDREFs differ.

`void append(const GTI& other)`
:   Append `other`'s intervals to this GTI's list without sorting or
    merging. Caller is expected to call `order()` afterwards if the
    result needs to be sorted.

`void append(const heasp::Real Start, const heasp::Real End)`
:   Append a single `(Start, End)` interval.

`heasp::Real exposure(const heasp::Real t1, const heasp::Real t2) const`
:   Returns the total exposure (sum of GTI-clipped interval lengths)
    inside `[t1, t2]`.

`void changeMJDREF(heasp::Integer MJDREF_days_in, heasp::Real MJDREF_seconds_in)`
:   Re-base the intervals onto a new MJDREF. Each interval start and
    stop is translated by the difference; the new MJDREF is stored
    in the object.

`heasp::Integer read(std::string filename, std::string gtiname)`
:   Read a GTI from a file. Tries FITS first (`readFITS`), then text
    (`readText`), then XRONOS (`readXRONOS`); the first format that
    succeeds wins. `gtiname` is the FITS extension name for the FITS
    branch; ignored by the others.

`heasp::Integer readFITS(std::string filename, std::string gtiname)`
:   Read from a FITS file. If `gtiname` is empty, the first GTI
    extension found is used. Reads only `START`, `STOP`, and
    `TIMEZERO`; deliberately ignores `MJDREF` keywords on the GTI
    extension itself, leaving the GTI's MJDREF at the `(0, 0)`
    sentinel for the caller to relabel. Returns `heasp::OK` on
    success.

`heasp::Integer readXRONOS(std::string filename)`
:   Read from an XRONOS window file. The window file's reference MJD
    is parsed and stored as the GTI's MJDREF.

`heasp::Integer readText(std::string filename)`
:   Read from a two-column text file (`start stop` on each line,
    whitespace-separated, comments allowed via `#`).

`heasp::Integer writeFITS(std::string filename, std::string gtiname, bool setTimezero=false) const`
:   Write the GTI to a FITS file. If `setTimezero` is true, write a
    `TIMEZERO` keyword equal to the MJDREF seconds component;
    otherwise omit it.

`void enlarge(heasp::Real extraStart, heasp::Real extraEnd)`
:   Extend each interval by `extraStart` seconds at the start and
    `extraEnd` seconds at the end.

`void removeSmallIntervals(heasp::Real minInterval)`
:   Drop any interval shorter than `minInterval` seconds.

`void removeSmallGaps(heasp::Real minGap)`
:   Merge any two adjacent intervals separated by less than `minGap`
    seconds.

### GTI public inline get/set methods

The class exposes get/set accessors for each private field. The
get-collection accessors return const references; the set-collection
accessors copy in-place and return `heasp::OK` (no validation). The
element accessors range-check; out-of-range indices return
`(-999.0, -999.0)` from getters and `heasp::VectorIndexOutsideRange`
from setters.

| Member | Get | Set |
|--------|-----|-----|
| `m_Intervals` | `getIntervals()`, `getIntervalsElement(i)` | `setIntervals(v)`, `setIntervalsElement(i, v)` |
| `m_MJDREF_days` | `getMJDREF_days()` | `setMJDREF_days(v)` |
| `m_MJDREF_seconds` | `getMJDREF_seconds()` | `setMJDREF_seconds(v)` |
| (start times only) | `getStart()`, `getStartElement(i)` | `setStart(v)`, `setStartElement(i, v)` |
| (end times only) | `getEnd()`, `getEndElement(i)` | `setEnd(v)`, `setEndElement(i, v)` |

`setMJDREF_days` and `setMJDREF_seconds` *relabel* the MJDREF without
translating the intervals. Use `changeMJDREF` if you want intervals
translated to a new MJDREF.

## GTIs

A container of named `GTI` objects, one per chip in the typical
multi-chip case. Each entry has a string identifier (used to match
chip IDs to GTIs), a FITS extension name, and an HDU name.

### GTIs private members

| Type | Member | Description |
|------|--------|-------------|
| `std::vector<GTI>` | `m_gti` | The GTIs |
| `std::vector<std::string>` | `m_stringID` | Per-GTI string identifier (typically a chip number) |
| `std::vector<std::string>` | `m_extname` | Per-GTI EXTNAME |
| `std::vector<std::string>` | `m_hduname` | Per-GTI HDUNAME |

### GTIs public methods

`int read(const std::string filename)`
:   Read every GTI extension found in `filename`. The extensions are
    identified by `HDUCLAS1=GTI` or extname patterns; both `STDGTI`
    and `GTI` extensions are recognised.

`int assignIDs(const dataSubspace& dskeys, const std::string tColName, const std::string idColName)`
:   Walk the data-subspace keys looking for entries that pair the
    time column `tColName` (DSVAL = "TABLE", DSREF naming a GTI
    extension) with chip-column `idColName` (DSVAL listing chip IDs).
    For each match, copy the chip ID into `m_stringID` of the
    corresponding GTI. After this call, `gtiFilter` can build its
    chip-ID → GTI lookup.

`size_t size() const`
:   Number of GTIs in the container.

`heasp::Integer totalNumberIntervals() const`
:   Sum of `gti.size()` across every GTI.

`double startTime() const`
:   Minimum start time across all intervals of all GTIs.

`double endTime() const`
:   Maximum end time across all intervals of all GTIs.

`void setStringID(const size_t i, const std::string id)` / `std::string getStringID(const size_t i) const`
:   Get/set the string identifier of the `i`-th GTI.

`void setExtname(...)` / `getExtname(...)` / `setHduname(...)` / `getHduname(...)`
:   Get/set the EXTNAME / HDUNAME of the `i`-th GTI.

`GTI& getGTI(const size_t i)` / `const GTI& getGTI(const size_t i) const`
:   Return a reference to the `i`-th GTI.

`GTI& getGTIbyID(const std::string stringIdentifier)` / `const GTI& getGTIbyID(...) const`
:   Return a reference to the GTI whose string identifier matches.
    Returns `m_gti[0]` if no match.

`void clear()`
:   Empty every member vector.

`void unify()`
:   Walk all GTIs and rebase them onto the earliest MJDREF found
    across the set, translating intervals as needed. Used by
    `eventInfo::combine` to harmonise GTIs read from different files.

`void order()`
:   Call `order()` on every GTI in turn.

`void merge(const GTI& other, bool andmode)`
:   Merge `other` into every GTI in this container.

`void merge(const GTIs& other, bool andmode)`
:   Merge `other` into this container. Per-chip matching is by
    string identifier when both sides have non-empty IDs; otherwise
    a positional pair-up. AND-mode requires both sides sorted; OR-mode
    is more permissive.

`std::string disp() const`
:   Multi-line dump for diagnostics.

## imageInfo

Image WCS metadata for one of the two image-coordinate frames in an
event file (the file's image frame and its WMAP frame). `eventInfo`
holds two `imageInfo` instances.

### imageInfo private members

| Type | Member | Description |
|------|--------|-------------|
| `std::string` | `m_colNameX`, `m_colNameY` | Column names for X and Y |
| `bool` | `m_goodWCS` | Whether a usable WCS was read from the file |
| `double` | `m_crpixX`, `m_crpixY` | CRPIX values |
| `double` | `m_crvalX`, `m_crvalY` | CRVAL values |
| `double` | `m_crdeltX`, `m_crdeltY` | CDELT values |
| `double` | `m_crota` | CROTA |
| `double` | `m_opticX`, `m_opticY` | Optical axis position in pixel coords |
| `std::string` | `m_ctypeX`, `m_ctypeY` | CTYPE values |
| `std::string` | `m_cnameX`, `m_cnameY` | CNAME values |
| `double` | `m_xMin`, `m_xMax`, `m_yMin`, `m_yMax` | TLMIN/TLMAX bounds |

### imageInfo public methods

`int read(CCfits::ExtHDU& eventsExt, const std::string xColName, const std::string yColName, const std::string xSizeKey, const std::string ySizeKey)`
:   Read WCS metadata for the two named columns from a CCfits
    `ExtHDU`. The `xSizeKey` / `ySizeKey` arguments name keywords to
    fall back on for `TLMIN/TLMAX` when those are missing (used for
    DETX/DETY in mission-specific cases). Returns 0 on success or
    the CCfits status on failure. `m_goodWCS` is set to false when
    the WCS keywords are absent.

`std::string disp() const`
:   Multi-line dump.

`bool compare(const imageInfo& beta) const`
:   Strict equality check across every member except CRPIX (which is
    deliberately tolerated; see DESIGN.md). Returns true if the two
    frames are identical for combine purposes.

`void compareForCombine(const imageInfo& beta, const std::string& tag, std::vector<std::string>& mismatches) const`
:   Like `compare`, but appends a human-readable mismatch description
    for every differing field to the `mismatches` vector instead of
    short-circuiting on the first difference. `tag` distinguishes the
    image and wmap frames in the output strings.

`int pixelToSky(double x, double y, double& ra, double& dec) const`
:   CFITSIO `ffwldp` wrapper. Converts a pixel coordinate to (RA,
    Dec) in the frame's CTYPE projection. Returns the CFITSIO status
    code.

`int pixelToSky(const std::vector<double>& x, const std::vector<double>& y, std::vector<double>& ra, std::vector<double>& dec) const`
:   Block overload. Sizes are not checked; caller's responsibility.

`int skyToPixel(double ra, double dec, double& x, double& y) const`
:   CFITSIO `ffxypx` wrapper.

`int skyToPixel(const std::vector<double>& ra, const std::vector<double>& dec, std::vector<double>& x, std::vector<double>& y) const`
:   Block overload.

`int transformTo(const imageInfo& dest, double xSrc, double ySrc, double& xDest, double& yDest) const`
:   Convert a pixel coordinate from this frame to `dest`'s frame.
    Equivalent to `this->pixelToSky` followed by `dest.skyToPixel`.
    Used by drivers that need to harmonise per-file image frames
    (ftextractor does this when combining files with different
    CRPIX values).

`int transformTo(const imageInfo& dest, const std::vector<double>& xSrc, const std::vector<double>& ySrc, std::vector<double>& xDest, std::vector<double>& yDest) const`
:   Block overload. Resizes the output vectors.

### imageInfo public inline get/set methods

Every private member has paired `getMember()` / `setMember(value)`
accessors. Setters return `heasp::OK`. The full set:

| Member | Getter | Setter |
|--------|--------|--------|
| `m_colNameX` | `getColNameX()` | `setColNameX(s)` |
| `m_colNameY` | `getColNameY()` | `setColNameY(s)` |
| `m_goodWCS` | `getGoodWCS()` | `setGoodWCS(b)` |
| `m_crpixX` | `getCrpixX()` | `setCrpixX(d)` |
| `m_crpixY` | `getCrpixY()` | `setCrpixY(d)` |
| `m_crvalX` | `getCrvalX()` | `setCrvalX(d)` |
| `m_crvalY` | `getCrvalY()` | `setCrvalY(d)` |
| `m_crdeltX` | `getCrdeltX()` | `setCrdeltX(d)` |
| `m_crdeltY` | `getCrdeltY()` | `setCrdeltY(d)` |
| `m_crota` | `getCrota()` | `setCrota(d)` |
| `m_opticX` | `getOpticX()` | `setOpticX(d)` |
| `m_opticY` | `getOpticY()` | `setOpticY(d)` |
| `m_ctypeX` | `getCtypeX()` | `setCtypeX(s)` |
| `m_ctypeY` | `getCtypeY()` | `setCtypeY(s)` |
| `m_cnameX` | `getCnameX()` | `setCnameX(s)` |
| `m_cnameY` | `getCnameY()` | `setCnameY(s)` |
| `m_xMin` | `getXMin()` | `setXMin(d)` |
| `m_xMax` | `getXMax()` | `setXMax(d)` |
| `m_yMin` | `getYMin()` | `setYMin(d)` |
| `m_yMax` | `getYMax()` | `setYMax(d)` |

## eventInfo

The largest class in heaev. Holds the per-file metadata read from an
event extension: image and WMAP `imageInfo`s, time-system keywords,
detector and mission identifying keywords, energy/chip/grade column
identities and bounds, dataSubspace keys, and the GTIs.

### eventInfo private members

| Type | Member | Description |
|------|--------|-------------|
| `imageInfo` | `m_image` | Image-coord WCS info |
| `imageInfo` | `m_wmap` | WMAP-coord WCS info |
| `double` | `m_imagePixelSize` | Pixel size in image coords |
| `std::vector<double>` | `m_pointingPos` | RA_PNT, DEC_PNT, PA_PNT |
| `int` | `m_referenceDay` | MJD reference day component |
| `double` | `m_referenceTime` | MJD reference fraction-of-day component |
| `bool` | `m_qMJDREF` | True if MJDREF was read as a single keyword; false if read as MJDREFI+MJDREFF |
| `double` | `m_timeDel` | Time resolution (TIMEDEL) |
| `double` | `m_timePixr` | TIMEPIXR (0.0 / 0.5 / 1.0) |
| `std::string` | `m_timeColName` | Time column name (typically `TIME`) |
| `std::string` | `m_timeref` | TIMEREF keyword |
| `std::string` | `m_timesys` | TIMESYS keyword |
| `std::string` | `m_timeunit` | TIMEUNIT keyword |
| `double` | `m_deadTimeCorr` | Dead-time correction factor |
| `double` | `m_equinox` | Equinox keyword |
| `std::string` | `m_dateObs`, `m_timeObs`, `m_dateEnd`, `m_timeEnd` | DATE/TIME-OBS/END |
| `std::string` | `m_instrument`, `m_telescope`, `m_filter`, `m_datamode`, `m_detector`, `m_target` | Identifying keywords |
| `std::string` | `m_energyColName` | Energy column |
| `double` | `m_energyColMin`, `m_energyColMax` | Energy column TLMIN / TLMAX |
| `std::string` | `m_chipColName` | Chip column (when each chip has its own GTI) |
| `double` | `m_chipColMin`, `m_chipColMax` | Chip column TLMIN / TLMAX |
| `std::string` | `m_gradeColName` | Grade column |
| `double` | `m_gradeColMin`, `m_gradeColMax` | Grade column TLMIN / TLMAX |
| `std::string` | `m_raDecSys` | RADECSYS keyword |
| `int` | `m_scseqend`, `m_numobis` | ROSAT-specific keywords |
| `std::vector<std::string>` | `m_mform`, `m_mtype` | MFORM / MTYPE keywords |
| `dataSubspace` | `m_dsKeys` | Data-subspace keys |
| `GTIs` | `m_gtis` | Per-chip GTIs |

### eventInfo public methods

`int read(const std::string infile, const std::string imageXcol, ...)`
:   Convenience for the single-file case. Calls `readFromOneFile`
    internally. The full argument list is identical.

`int readFromOneFile(const std::string infile, const std::string imageXcol, const std::string imageYcol, const std::string wmapXcol, const std::string wmapYcol, const std::string tCol, const std::string eCol, const std::string cCol, const std::string gCol, const std::string eventName, const std::string mphakey, const std::string xfkey, const std::string yfkey, const std::string xhkey, const std::string yhkey)`
:   Read every header field from the named events extension of
    `infile`. The 13 string arguments give the column names for the
    image (X, Y), WMAP (X, Y), time, energy, chip, and grade
    columns, the events-extension name, and five fallback keyword
    names used when TLMIN/TLMAX are missing (energy, image-X
    size, image-Y size, WMAP-X size, WMAP-Y size). Returns 0 on
    success.

`combineResult combine(const eventInfo& beta)`
:   Merge another file's metadata into this one per the A/B/C
    taxonomy. The returned struct has `ok=false` and a list of
    mismatch descriptions if any Class-A field disagreed; the
    in-place merge of Class-B and Class-C fields proceeds whether
    `ok` is true or false. The CRPIX components of the image and
    WMAP WCS are deliberately excluded from the strict-match Class
    A; per-file CRPIX is the orchestration layer's concern.

`std::string disp() const`
:   Multi-line dump for diagnostics.

`struct combineResult`
:   Small POD with `bool ok` and `std::vector<std::string>
    mismatches`. Returned by `combine`.

### eventInfo public inline get/set methods

Every private member has paired accessors. The full table:

| Member | Getter | Setter |
|--------|--------|--------|
| `m_image` | `getImage()` | `setImage(imageInfo)` |
| `m_wmap` | `getWmap()` | `setWmap(imageInfo)` |
| `m_imagePixelSize` | `getImagePixelSize()` | `setImagePixelSize(d)` |
| `m_pointingPos` | `getPointingPos()` | `setPointingPos(v)` |
| `m_referenceDay` | `getReferenceDay()` | `setReferenceDay(i)` |
| `m_referenceTime` | `getReferenceTime()` | `setReferenceTime(d)` |
| `m_qMJDREF` | `getQMJDREF()` | `setQMJDREF(b)` |
| `m_timeDel` | `getTimeDel()` | `setTimeDel(d)` |
| `m_timePixr` | `getTimePixr()` | `setTimePixr(d)` |
| `m_timeColName` | `getTimeColName()` | `setTimeColName(s)` |
| `m_timeref` | `getTimeref()` | `setTimeref(s)` |
| `m_timesys` | `getTimesys()` | `setTimesys(s)` |
| `m_timeunit` | `getTimeunit()` | `setTimeunit(s)` |
| `m_deadTimeCorr` | `getDeadTimeCorr()` | `setDeadTimeCorr(d)` |
| `m_equinox` | `getEquinox()` | `setEquinox(d)` |
| `m_dateObs` | `getDateObs()` | `setDateObs(s)` |
| `m_timeObs` | `getTimeObs()` | `setTimeObs(s)` |
| `m_dateEnd` | `getDateEnd()` | `setDateEnd(s)` |
| `m_timeEnd` | `getTimeEnd()` | `setTimeEnd(s)` |
| `m_instrument` | `getInstrument()` | `setInstrument(s)` |
| `m_telescope` | `getTelescope()` | `setTelescope(s)` |
| `m_filter` | `getFilter()` | `setFilter(s)` |
| `m_datamode` | `getDatamode()` | `setDatamode(s)` |
| `m_detector` | `getDetector()` | `setDetector(s)` |
| `m_target` | `getTarget()` | `setTarget(s)` |
| `m_energyColName` | `getEnergyColName()` | `setEnergyColName(s)` |
| `m_energyColMin` | `getEnergyColMin()` | `setEnergyColMin(d)` |
| `m_energyColMax` | `getEnergyColMax()` | `setEnergyColMax(d)` |
| `m_chipColName` | `getChipColName()` | `setChipColName(s)` |
| `m_chipColMin` | `getChipColMin()` | `setChipColMin(d)` |
| `m_chipColMax` | `getChipColMax()` | `setChipColMax(d)` |
| `m_gradeColName` | `getGradeColName()` | `setGradeColName(s)` |
| `m_gradeColMin` | `getGradeColMin()` | `setGradeColMin(d)` |
| `m_gradeColMax` | `getGradeColMax()` | `setGradeColMax(d)` |
| `m_raDecSys` | `getRaDecSys()` | `setRaDecSys(s)` |
| `m_scseqend` | `getScseqend()` | `setScseqend(i)` |
| `m_numobis` | `getNumobis()` | `setNumobis(i)` |
| `m_mform` | `getMform()` | `setMform(v)` |
| `m_mtype` | `getMtype()` | `setMtype(v)` |
| `m_dsKeys` | `getDsKeys()` | `setDsKeys(dataSubspace)` |
| `m_gtis` | `getGtis()` | `setGtis(GTIs)` |

### Free helpers

`std::string compareDateString(const std::string& dateString1, const std::string& dateString2, bool first)`
:   Compare two `YYYY-MM-DD`-style date strings. Returns the earlier
    string if `first=true`, the later string otherwise. Used by
    `eventInfo::combine` for `DATE-OBS` / `DATE-END` rules.

`std::string compareTimeString(const std::string& timeString1, const std::string& timeString2, bool first)`
:   Like `compareDateString` for `HH:MM:SS`-style time strings.

## dataSubspace

The data-subspace keyword machinery. Reads `DSTYPi` / `DSVALi` /
`DSFORMi` / `DSUNITi` / `DSREFi` keywords from an events extension,
compiles them into a `dsFilter` predicate, supports set-union of
chip-column values across files, and prunes chip entries that
contributed no events.

### dataSubspace private members

| Type | Member | Description |
|------|--------|-------------|
| `std::vector<std::string>` | `m_type` | DSTYPi (column names) |
| `std::vector<std::vector<std::string>>` | `m_value` | jDSVALi (per-key list of allowed values / ranges) |
| `std::vector<std::string>` | `m_form` | DSFORMi (numeric format hint) |
| `std::vector<std::string>` | `m_unit` | DSUNITi (units) |
| `std::vector<std::vector<std::string>>` | `m_ref` | jDSREFi (reference HDU names, used for TABLE refs) |
| `std::vector<int>` | `m_id` | Per-key identifier number from the FITS keys |

### dataSubspace public methods

`int read(CCfits::ExtHDU& eventsExt)`
:   Read every DS keyword from the extension. Returns 0 on success
    or the CCfits status code on failure.

`std::unique_ptr<dsFilter> buildFilter(const std::string& timeColName) const`
:   Compile the DS spec into a fast per-event predicate. Skips
    entries whose `DSVAL` is `"TABLE"` (those are gtiFilter's
    responsibility) and any column whose name matches `timeColName`
    (also handled by gtiFilter). Numeric clauses understand single
    values, `lo:hi` ranges, and comma-separated lists; unparseable
    clauses are silently dropped. Returns a non-null
    `unique_ptr<dsFilter>` even when the resulting filter is empty
    (`empty()` will be true in that case).

`void prune(const std::vector<bool>& qinreg, const std::string& chipColName)`
:   Drop chip-column DS values whose corresponding GTI did not
    contribute any events. `qinreg[i]` is `false` for the `i`-th GTI
    that should be pruned. Other columns are left alone. This is
    called after iteration completes and before writing output
    extensions.

`size_t size() const`
:   Number of DS keys.

`std::string disp() const`
:   Multi-line dump.

`bool compare(const dataSubspace& beta) const`
:   Returns true if two `dataSubspace`s are identical.

`void mergeChipUnion(const dataSubspace& beta, const std::string& chipColName)`
:   Set-union the chip-column values from `beta` into `*this`. Other
    columns are left alone. Used by `eventInfo::combine` to merge
    per-chip DS specs across input files.

### dataSubspace public inline get/set methods

| Member | Getter | Setter |
|--------|--------|--------|
| `m_type` | `getType()` | `setType(v)` |
| `m_value` | `getValue()` | `setValue(v)` |
| `m_form` | `getForm()` | `setForm(v)` |
| `m_unit` | `getUnit()` | `setUnit(v)` |
| `m_ref` | `getRef()` | `setRef(v)` |
| `m_id` | `getId()` | `setId(v)` |

## filterChain

The orchestrator. Holds an ordered list of filters, runs each in
turn, and tracks per-filter rejection counts via first-rejecter
attribution.

### filterChain private members

| Type | Member | Description |
|------|--------|-------------|
| `std::vector<std::unique_ptr<filter>>` | `m_filters` | The chain |
| `mutable std::map<std::string, long>` | `m_rejections` | Per-filter rejection counts, keyed by `filter::name()` |

### filterChain public methods

`filterChain()`, `~filterChain()`
:   Default-constructible, default-destructible. Non-copyable
    (the chain owns its filters); movable.

`void add(std::unique_ptr<filter> f)`
:   Take ownership of `f` and append it to the chain.

`size_t size() const`
:   Number of filters in the chain.

`void compute(const eventBlock& block, std::vector<bool>& mask) const`
:   Reset `mask` to `block.size` `true` values and apply each filter
    in order. After each filter, count the bits that were flipped
    from `true` to `false` and add them to that filter's rejection
    counter. The mask is the AND of every filter's decision.

`std::vector<bool> compute(const eventBlock& block) const`
:   Convenience overload that returns a fresh mask.

`std::set<std::string> requiredColumns() const`
:   Union of `requiredColumns()` across every filter in the chain.
    Used by `eventReader` to build the column-materialisation set.

`const std::map<std::string, long>& rejectionCounts() const`
:   The per-filter rejection map. Each event is attributed to the
    *first* filter in the chain that flipped its mask bit, so
    summing the values gives the total events rejected by the chain.

`void resetRejectionCounts()`
:   Empty the rejection map.

## eventReader

Block-oriented event-file iterator. Opens a single input file at
construction and provides `iterate(chain, accumulators)`. The reader
determines the column-materialisation set from
`chain.requiredColumns()` ∪ `accumulator->requiredColumns()`.

### eventReader private members

| Type | Member | Description |
|------|--------|-------------|
| `std::unique_ptr<CCfits::FITS>` | `m_fits` | Owned FITS handle |
| `CCfits::ExtHDU*` | `m_eventsExt` | Pointer into `m_fits`; not owned |
| `long` | `m_numRows` | Total rows in the events extension |
| `long` | `m_stride` | Block size; default = `ExtHDU::getRowsize()` |
| `int` | `m_status` | 0 if construction succeeded |
| `size_t` | `m_fileIndex` | Stamped onto each block |
| `std::map<std::string, std::string>` | `m_lowerToCanonical` | Case-insensitive column-name lookup |

### eventReader public methods

`eventReader(const std::string& filename, const std::string& eventsExtName = "EVENTS")`
:   Open `filename` and navigate to the events extension. Tries
    `eventsExtName` first; if that extension does not exist, scans
    HDUs for `HDUCLAS1=EVENTS`. On failure, `status()` returns
    non-zero and `iterate()` is a no-op.

`~eventReader()`
:   Closes the FITS file.

`bool good() const`
:   True if construction succeeded.

`int status() const`
:   Construction status; 0 on success.

`long numRows() const`
:   Number of rows in the events extension.

`long stride() const`
:   Current block size.

`void setStride(long s)`
:   Set the block size. Values ≤ 0 are ignored.

`size_t fileIndex() const`
:   The file index stamped onto each block produced by this reader.
    Defaults to 0.

`void setFileIndex(size_t i)`
:   Set the file index. Multi-file drivers call this once per
    reader before `iterate`.

`long iterate(const filterChain& chain, const std::vector<eventAccumulator*>& accumulators)`
:   Walk the events extension in stride-sized blocks. For each
    block: read the columns required by `chain` and `accumulators`,
    compute the pass mask via `chain.compute`, and call
    `accumulator->update(block, mask)` for every accumulator.
    Returns the total number of rows processed (`== numRows()`
    on a complete pass), or `-1` on read error.

The reader is non-copyable; pass it by reference if a subroutine
needs access during iteration.

---

## Filters

Every filter implements the abstract `filter` interface. The chain
runs them in `add` order, AND-folding each one's mask into the
running result. A filter that wants to reject an event sets
`mask[i] = false`; a filter that has nothing to say leaves the bit
alone.

### filter (abstract base)

`virtual void compute(const eventBlock& block, std::vector<bool>& mask) const = 0`
:   Compute the per-event pass mask for one block. Implementations
    should AND their decision into `mask`, not replace it. The mask
    arrives pre-initialised to `true` on the first filter and as
    the running fold thereafter.

`virtual std::set<std::string> requiredColumns() const = 0`
:   The block columns this filter needs. Used by `eventReader` to
    decide which columns to materialise.

`virtual std::string name() const`
:   Human-readable name used as the key in
    `filterChain::rejectionCounts()`. Defaults to `"filter"`;
    subclasses typically override.

The `mask` vector is always sized to `block.size` before any filter
is called. Filters may write false where they want to reject; they
must not resize the mask.

Subclasses are non-copyable in practice (they typically own resources
like an opened SAORegion or a parsed bitmap). The chain takes
ownership via `unique_ptr`.

### gtiFilter

Per-event GTI predicate. Each event's chip ID maps to a per-chip
`GTI` looked up at construction. Events whose chip ID has no matching
GTI fail.

`gtiFilter(const GTIs& gtis, const std::string& timeCol, const std::string& chipCol, int eventMjdRefDays, double eventMjdRefSeconds)`
:   Construct the chip-ID → GTI lookup from the GTIs container. Each
    GTI's `stringID` is interpreted as an integer chip ID via
    `std::stoi`; entries with non-integer IDs are skipped. The
    `(eventMjdRefDays, eventMjdRefSeconds)` argument is the events'
    MJDREF in `(int days, seconds-into-day-as-double)` form.

`void compute(...)` / `std::set<std::string> requiredColumns() const` / `std::string name() const`
:   Inherited overrides. `name()` returns `"gtiFilter"`. The required
    columns are the time column and the chip column.

If `gtis` has only one entry and no resolvable chip IDs, the filter
falls back to a default-GTI mode that ignores the chip column and
applies the lone GTI to every event.

### phaseFilter

Per-event phase-window predicate driven by an XRONOS `.wnd` file.
Inactive (full passthrough) until configured.

`phaseFilter()`
:   Default-construct. The filter is inactive until
    `readWindowFile` succeeds.

`int readWindowFile(const std::string& filename, double tjdMJD)`
:   Read the `.wnd` file. `tjdMJD` is the MJD of "day 0" in the
    `.wnd`'s time system (typically 40000.0 for TJD). Phase windows
    are stored raw; `setEventMJDRef` must be called before
    `compute`. Returns 0 on success, non-zero on file-read error.

`void setEventMJDRef(int eventDays, double eventSeconds)`
:   Set the events' MJDREF in `(int days, seconds-into-day)` form
    and recompute the phase-folding epoch in seconds.

`void setTimeColumn(const std::string& col)`
:   Override the default time column name (`TIME`).

`bool active() const`
:   False if no phase windows were read; in that case `compute` is
    a no-op.

`double correctedExposure(double gtiStart, double gtiStop) const`
:   Phase-corrected exposure of `[gtiStart, gtiStop]` (seconds),
    summed across all phase windows. Returns the unmodified GTI
    length when the filter is inactive or has no period. Mirrors
    the Fortran extractor's `CALC_EXP` / `EXPCORR` logic from
    `times.f`.

`void compute(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` returns `"phaseFilter"`.

### gradeFilter

Per-event grade-bitmap predicate. Parses a numeric-only spec string
into a fixed-size bitmap; events whose grade column rounds to a true
bit pass.

Spec syntax (matching the Fortran extractor's `grade.f`):

- `0,2,3` --- explicit list
- `0-3` --- inclusive range
- `>5` --- any grade strictly greater than 5
- `<2` --- any grade strictly less than 2
- `0,2-4,>10` --- any combination, comma-separated

No mission-named presets (no `"GRADE0_4"` or similar).

`static const size_t MAX_GRADE = 256`
:   Matches the Fortran's `GMAXSIZE`.

`gradeFilter()`
:   Default-construct, inactive.

`void setBounds(int gmin, int gmax)`
:   Set the valid grade range from the file's `TLMIN`/`TLMAX`. Values
    outside `[gmin, gmax]` always fail.

`void setGradeColumn(const std::string& col)`
:   Override the grade-column name.

`int parse(const std::string& spec)`
:   Parse the spec string. Empty or `"NONE"` leaves the filter
    inactive. Returns 0 on success.

`bool active() const`
:   False until `parse` succeeds.

`compute(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"gradeFilter"`.

### keyvalFilter

Per-event predicate for the `[KEY=lo:hi,...]` filename argument
syntax. Fortran semantics: AND across keys with different names,
OR across multiple ranges with the same name. So `[X=10:20,X=50:60]`
passes events with X in `[10,20]` or `[50,60]`; `[X=10:20,Y=5:7]`
needs both.

`static constexpr double NO_BOUND = -99999.0`
:   The legacy sentinel meaning "no bound on this side". Produced by
    `eventFilenameParse` for the `*` syntax in the original event
    filename grammar.

`using range = std::pair<double, double>`
:   `(lo, hi)` pair.

`using ranges = std::vector<range>`
:   Multiple ranges for one key.

`using spec = std::map<std::string, ranges>`
:   The full specification.

`keyvalFilter()`
:   Default-construct, empty spec.

`explicit keyvalFilter(const spec& s)`
:   Construct with an initial spec.

`void setSpec(const spec& s)`
:   Replace the spec.

`void addRange(const std::string& key, double lo, double hi)`
:   Append one range to a key (creates the key if absent).

`void addLegacy(const std::map<std::string, std::vector<double>>& legacy)`
:   Convenience for callers that produce one range per key as a
    `vector<double>` of size 2 --- the format `eventFilenameParse`
    returns. Each entry's vector must have exactly two elements.

`bool active() const`
:   True iff the spec is non-empty.

`compute(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"keyvalFilter"`. Required
    columns are the names of every key in the spec.

### dsFilter

Fast per-event predicate compiled from a `dataSubspace`. Built
indirectly via `dataSubspace::buildFilter`, not by the user. Drivers
register the result in their chain just like any other filter.

`struct clause { double lo; double hi; }`
:   A single allowed range for one DS column. `lo == hi` for a
    single-value test.

`struct columnPredicate { std::string column; std::vector<clause> clauses; }`
:   One column's full predicate (a list of allowed clauses, OR-ed).

`dsFilter()`
:   Default-construct, no predicates.

`void addColumnPredicate(const columnPredicate& p)`
:   Append a column predicate. Different columns AND together.

`size_t size() const` / `bool empty() const`
:   Number of column predicates.

`compute(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"dsFilter"`. Required columns
    are the columns named in every predicate.

The `TABLE`-reference case (e.g. `TIME` with `DSVAL="TABLE"` and a GTI
extension named in `DSREF`) is the `gtiFilter`'s responsibility and
is skipped during `buildFilter`. The chip-column case is also
typically enforced by `gtiFilter` via its per-chip GTI lookup; the
`dsFilter` still applies the discrete-value test as a belt-and-braces
check.

### regionFilter

Wraps the CFITSIO region API (`fits_read_rgnfile` / `fits_in_region`).
Holds an opened `SAORegion`; non-copyable. Unlike the other filters,
`regionFilter` is *not* a `filter` subclass and cannot be added
directly to the chain. Drivers that want region rejection in the
chain wrap it in a small adapter (see Example 2 in the Pipeline
overview).

`regionFilter(const std::string& filename, const imageInfo& wcs)`
:   Open `filename` as a DS9-style region or FITS region extension.
    The `wcs` is used to interpret world-coordinate regions. The
    constructor stores any error from `fits_read_rgnfile` in
    `m_status`; callers must check `status()` before using the
    predicate.

`~regionFilter()`
:   Releases the SAORegion.

`bool good() const` / `int status() const`
:   Construction status check.

`bool inRegion(double x, double y) const`
:   Per-event predicate. Returns false if the constructor failed.

`void inRegion(const std::vector<double>& x, const std::vector<double>& y, std::vector<bool>& mask) const`
:   Block predicate. Resizes `mask` to match input.

`std::vector<shape> shapes() const`
:   Returns the region as a list of `shape` records, used by output
    writers that want to emit a FITS region BINTABLE. Each shape
    carries its CFITSIO sign (`+`/`-`), shape-type enum, type name,
    component number, and parameter array. Mirrors the Fortran
    `w_regions.f`.

```cpp
struct shape {
  char        sign;       // '+' include / '-' exclude
  int         type;       // CFITSIO shapeType enum value
  std::string typeName;   // 'CIRCLE', 'BOX', 'POLYGON', ...
  int         component;  // shape component number
  std::vector<double> params;  // gen.p[0..10] or polygon Pts
};
```

---

## Accumulators

Every accumulator implements the abstract `eventAccumulator`
interface. The reader hands `(block, mask)` to each accumulator in
turn for every block. Accumulators only consider events with
`mask[i] == true`.

Output formats are not the accumulators' responsibility. Each
accumulator owns in-memory state and exposes accessors; the driver
program reads that state and writes whatever format it wants.

### eventAccumulator (abstract base)

`virtual void update(const eventBlock& block, const std::vector<bool>& mask) = 0`
:   Update internal state from one block. Implementations skip
    events with `mask[i] == false`. `block.size` always matches
    `mask.size()`.

`virtual std::set<std::string> requiredColumns() const = 0`
:   The block columns this accumulator needs.

`virtual std::string name() const`
:   Diagnostic name; defaults to `"eventAccumulator"`.

### imageAccumulator

2D X/Y histogram. Optional Z weighting and digitisation.

`imageAccumulator(const std::string& xCol, const std::string& yCol, int xLo, int yLo, int xHi, int yHi, int binFactor = 1, double xInt = 1.0, double yInt = 1.0)`
:   The bounding box `(xLo, yLo, xHi, yHi)` is in unbinned pixel
    coordinates. The output image dimensions are
    `floor((xHi - xLo + 1) / binFactor)` by
    `floor((yHi - yLo + 1) / binFactor)`. `xInt`/`yInt` scale the raw
    column values before binning (e.g. `xInt=2` means each unbinned
    pixel covers two raw detector positions).

`void setZColumn(const std::string& zCol)`
:   Enable a Z-weighted variant. Once enabled, `update` accumulates
    `imageZ[iy][ix] += z[i]` for every passing event. Caller divides
    `imageZ` by `image` after iteration to get the Z-weighted image.

`const std::vector<std::vector<double>>& image() const`
:   The 2D count image.

`const std::vector<std::vector<double>>& imageZ() const`
:   The Z-weighted accumulator (empty if no Z column was set).

`size_t pixelsHit() const`
:   Number of pixels that received at least one count.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"imageAccumulator"`.

### spectrumAccumulator

1D energy histogram.

`spectrumAccumulator(const std::string& eCol, int eLo, int eHi, int specBin = 1)`
:   Bin `k` covers `[eLo + k*specBin, eLo + (k+1)*specBin - 1]`.
    The spectrum size is `ceil((eHi - eLo + 1) / specBin)`.

`const std::vector<double>& spectrum() const`
:   Counts per bin.

`int firstChannel() const`
:   First PI channel of bin 0; equal to `eLo`. Useful for output
    writers that need to populate `TLMIN`-style metadata.

`int specBin() const`
:   Bin width supplied at construction.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"spectrumAccumulator"`.

### lightcurveAccumulator

1D time histogram with TIMEPIXR correction.

`lightcurveAccumulator(const std::string& tCol, double minTime, double binSec, size_t nBins, double timePixr = 0.5, double timeDel = 0.0)`
:   Bin `k` covers `[minTime + k*binSec, minTime + (k+1)*binSec)` in
    the events' time system. `timePixr` is the events' TIMEPIXR (0.0
    / 0.5 / 1.0 for start / midpoint / end of readout interval); the
    output FITS lightcurve convention is `TIMEPIXR=0.5`, so events
    are shifted by `(timePixr - 0.5) * timeDel`. `timeDel <= 0` turns
    the correction off.

`const std::vector<long>& counts() const`
:   Counts per bin. Note this is `long`, not `double` --- the
    lightcurve is integer-valued.

`double minTime() const` / `double binSec() const`
:   The values supplied at construction.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"lightcurveAccumulator"`.

### wmapAccumulator

2D X/Y histogram in WMAP coordinates. Same shape as
`imageAccumulator` but without the Z-weighting and digitisation
options. The WMAP coordinates are usually integer detector positions,
so digitisation is not relevant; keeping `imageAccumulator` simple
is the reason for the separate class.

`wmapAccumulator(const std::string& xCol, const std::string& yCol, int xLo, int yLo, int xHi, int yHi, int binFactor = 1)`
:   As for `imageAccumulator`, minus the Z and digitisation
    parameters.

`const std::vector<std::vector<double>>& wmap() const`
:   The 2D WMAP.

`size_t pixelsHit() const`
:   Pixels with at least one count.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"wmapAccumulator"`.

### wmapSumsAccumulator

Accumulates the nine moments used by the original `fixwmp` routine to
fit a linear relationship between image (X, Y) and WMAP (Xh, Yh)
pixel coordinates. Active only when image and WMAP coordinates
differ; the driver decides whether to register the accumulator.

The nine sums in Fortran order:

```
sum dx, sum dy, sum dxh, sum dyh,
sum dx*dxh, sum dx*dyh, sum dy*dxh, sum dy*dyh, count
```

where `dx = x - x0` and similar (offsets from the bbox centre). The
sums are populated only for events that pass the mask AND fall
within the image bounding box.

`wmapSumsAccumulator(const std::string& xCol, const std::string& yCol, const std::string& xhCol, const std::string& yhCol, double x0, double y0, double xh0, double yh0)`
:   Construct with image and WMAP column names plus the centring
    offsets `(x0, y0)` and `(xh0, yh0)` (typically the bbox
    centres).

`const std::vector<double>& sums() const`
:   The nine sums in the order listed above.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"wmapSumsAccumulator"`.

### unbinnedLcAccumulator

Per-event time/PI text dump for the unbinned-LC product. Writes one
line per passing event ("`<time> <round(pi)>`") to a caller-provided
`std::ostream`. The accumulator does not own the stream; the caller
opens / renames / closes it.

`unbinnedLcAccumulator(const std::string& tCol, const std::string& eCol, std::ostream* out)`
:   `out` may be null to disable output. The stream must outlive the
    accumulator.

`long count() const`
:   Number of events written.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"unbinnedLcAccumulator"`.

### rowIndexAccumulator

Per-event `(file_index, row_number)` recorder, backed by a binary
temp file. `update` appends 16 bytes per passing event:

```
uint64_t fileIndex
uint64_t rowNumber  // 1-based, CFITSIO convention
```

After iteration the caller calls `passedRows` to walk the
`(fileIndex, rowNumber)` pairs in append order. Per input file the
row numbers are monotonically increasing, which lets a writer like
`writeEventsEnd` batch them into contiguous CFITSIO row-copy ranges.

The temp file lives in `$TMPDIR` (or `/tmp`) under a name based on
the process pid plus an instance counter. The destructor removes it.

`rowIndexAccumulator()` / `~rowIndexAccumulator()`
:   Open / close the backing file. Non-copyable.

`bool good() const` / `int status() const`
:   Construction status.

`uint64_t count() const`
:   Number of events recorded so far.

`const std::string& tempPath() const`
:   Backing-file path, useful for diagnostics.

`int passedRows(const callback& cb)`
:   After iteration, walk the recorded rows in order. Closes the
    write end first (if still open) and re-opens for reading. The
    callback signature is
    `void(uint64_t fileIndex, uint64_t rowNumber)`. Returns 0 on
    success.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `requiredColumns()` returns an empty set
    (the accumulator records row indices, not column values).

### stokesAccumulator

I/Q/U sums per spectral and time bin. Either the spectral or the
lightcurve part can be disabled by passing zero size; mode `NONE`
is a fully-disabled instance.

The maintained sums per spectral and per time bin:

```
I[k]  += w_i
Q[k]  += q_i * w_i
U[k]  += u_i * w_i
I2[k] += w_i^2
Q2[k] += (q_i * w_i)^2
U2[k] += (u_i * w_i)^2
```

where `q_i` and `u_i` come from per-event Q and U columns and `w_i`
is the polarisation weight (column `polWeightCol`, or 1.0 if
UNWEIGHTED). The squared sums let the writers compute uncertainties
later. NEFF and SIMPLE modes differ only in how the output is
normalised; the accumulation is identical.

`enum class Mode { NONE, NEFF, SIMPLE, UNWEIGHTED }`
:   Selects the output normalisation. NONE disables the accumulator.

`struct config`
:   Bundle of configuration options:

    | Field | Type | Meaning |
    |-------|------|---------|
    | `mode` | `Mode` | `NONE` / `NEFF` / `SIMPLE` / `UNWEIGHTED` |
    | `qCol`, `uCol` | `string` | Q and U column names (default `"Q"`, `"U"`) |
    | `polWeightCol` | `string` | Polarisation weight column; empty for `UNWEIGHTED` |
    | `eCol`, `tCol` | `string` | Energy and time columns; empty when the corresponding side is inactive |
    | `eLo`, `eHi`, `specBin` | `int` | Spectral binning |
    | `specSize` | `size_t` | Spectral output size (0 disables) |
    | `minTime`, `binSec` | `double` | Lightcurve binning |
    | `lcBins` | `size_t` | Lightcurve output size (0 disables) |

`explicit stokesAccumulator(const config& cfg)`
:   Configure and allocate the per-bin sum vectors.

`bool active() const`
:   True iff `mode != NONE`.

`Mode mode() const`
:   The configured mode.

`const std::vector<double>& specI() const` / `specQ()` / `specU()` / `specI2()` / `specQ2()` / `specU2()`
:   Spectral sums.

`lcI()` / `lcQ()` / `lcU()` / `lcI2()` / `lcQ2()` / `lcU2()`
:   Lightcurve sums. Same return type.

`update(...)` / `requiredColumns()` / `name()`
:   Inherited overrides. `name()` is `"stokesAccumulator"`.

---

# Error handling

heaev does not have a uniform error type. The library returns errors
in whichever form fit each method's typical use:

- **Read methods on `GTI`, `GTIs`, `dataSubspace`, `imageInfo`, and
  `eventInfo`** return `int` or `heasp::Integer` status codes. `0`
  / `heasp::OK` indicates success; non-zero values are CCfits status
  codes when the read failed in the FITS layer, or
  `heasp::VectorIndexOutsideRange` for element-accessor range
  errors.

- **`eventReader::iterate` returns `long`.** Non-negative values are
  the number of rows processed; `-1` indicates a read error.
  `eventReader::status()` returns the construction status.

- **`regionFilter` and `rowIndexAccumulator`** are constructible
  even on failure; their `good()` and `status()` accessors report
  whether the constructor succeeded. Drivers must check before use.

- **`dataSubspace::buildFilter` returns `std::unique_ptr<dsFilter>`**
  that is always non-null; an empty resulting filter has
  `dsFilter::empty() == true`.

- **`eventInfo::combine` returns a `combineResult` struct** with
  `bool ok` and `vector<string> mismatches`. Class-A mismatches are
  collected without short-circuit.

- **Element accessors on `GTI`** return `(-999.0, -999.0)` from
  getters and `heasp::VectorIndexOutsideRange` from setters when the
  index is out of range.

- **Filter / accumulator `compute` and `update`** return `void`.
  They cannot signal failure to the chain; conditions that should
  abort iteration are typically validated at construction time.

- **`std::out_of_range` is thrown** by `eventBlock::column` if the
  caller asks for a column that was not materialised. This
  indicates a programming error: either `requiredColumns()` was
  incomplete or the column genuinely does not exist in the events
  extension. It is not a runtime user error.

For the codes used by status returns, see the *Error Codes* chapter
of the heasp guide; heaev uses the same constants (`heasp::OK`,
`heasp::VectorIndexOutsideRange`, etc.) and the same convention that
non-zero values from a CCfits-backed read are CCfits status codes.

A future version may introduce a uniform `heaev::error` exception
class for unrecoverable conditions, with an associated migration of
the read methods. The change-log chapter will record any such
change.

---

# Building programs using heaev

A heaev tool needs the heaev library and the libraries heaev itself
links against:

- `-lheaev` --- this library
- `-lheasp` (or whatever HEASP version-suffixed library name is in
  use; check `${HEASP}` in the build environment)
- `-lCCfits`
- `-lcfitsio`
- The standard heasoft heainit / heaio libraries when the tool uses
  the `headas_main`-style entry point: `-lhdsp`, `-lhdutils`,
  `-lhdio`, `-lhdinit` (with the version suffixes).

The headers are installed into `$HEADAS/include`. `#include` them
without path qualification:

```cpp
#include "eventInfo.h"
#include "eventReader.h"
#include "filterChain.h"
// ... etc.
```

The recommended Makefile pattern is the same as for any heasoft C++
task. ftextractor's Makefile is the canonical example:

```make
HD_COMPONENT_NAME       = heasptools

HD_COMPONENT_VERS       =

HD_CXXTASK              = mytool

HD_CXXTASK_SRC_cxx      = mytool.cxx ...

HD_CXXFLAGS             = ${HD_STD_CXXFLAGS}

HD_CXXLIBS              = ${HD_LFLAGS} -lheaev -l${HEASP} \
                          -l${CCFITS} ${HD_STD_LIBS} ${SYSLIBS}

HD_INSTALL_TASKS        = ${HD_CXXTASK}

HD_INSTALL_PFILES       = ${HD_CXXTASK}.par

HD_INSTALL_HELP         = ${HD_CXXTASK}.html

include ${HD_STD_MAKEFILE}
```

`HD_STD_LIBS` pulls in everything else (CFITSIO, the standard
heasoft initialisation libraries, the system libraries). The build
gives you a `mytool` binary, an installed `.par` file, and an
installed `.html` help page.

If your tool needs heasp::pha output (as Example 2 above does), the
heasp library is already on the link line via `-l${HEASP}`. No
further build changes.

heaev does not use exceptions across its public API except for the
explicit `std::out_of_range` from `eventBlock::column`, so callers
do not need to wrap every API call in a try/catch. Drivers that
prefer to translate exceptions to status codes typically wrap their
top-level entry function:

```cpp
int mytool() {
  try {
    // ... main work
    return 0;
  } catch (const std::exception& e) {
    headas_printf(("Unhandled exception: " + std::string(e.what()) + "\n").c_str());
    return -1;
  }
}
```

---

# Heaev clients

As of v0.1, the only consumer of heaev in the heasoft tree is
`ftextractor` (`heasptools/ftextractor/ftextractor.cxx`). Read the
ftextractor source as the long-form worked example for this guide.
Its DESIGN.md captures the architectural rationale and the
contracts that connect heaev to ftextractor's writer layer.

Future tools that walk an event file --- whether they emit images,
spectra, lightcurves, or filtered event lists --- should use heaev
for the per-event loop and contribute new accumulators or filters
upstream rather than rolling their own. Mission-specific behaviour
belongs in the `.par` file or in the input file's header keywords;
heaev itself remains mission-agnostic.

---

# Change log

**v0.1** --- Initial release. Provides the classes documented above,
supporting the ftextractor parity with the Fortran extractor task.
The API is *not* stable at this stage; minor-version bumps may
break source compatibility. Specific surfaces likely to evolve:

- The MJDREF API on `GTI` is asymmetric: some methods take
  `(int days, double seconds-into-day)`, others take
  `(int days, double fraction-of-day)`. A unified accessor will
  appear in a later version.
- `regionFilter` is not currently a `filter` subclass; drivers wrap
  it. A future version may unify the interface so it can be added
  to the chain directly.
- The error model is intentionally non-uniform; a `heaev::error`
  exception class is planned.
- No Python bindings yet. SWIG plumbing parallel to heasp's may
  appear in a future version.
