Metadata-Version: 2.2
Name: neidspec
Version: 0.2.1
Summary: Package to work with NEID Spectra
Home-page: https://github.com/TeHanHunter/neidspec
Author: Te Han, Gudmundur Stefansson (author of HPFSpec)
Author-email: tehanhunter@gmail.com
License: MIT
Keywords: NEID Spectra Astronomy
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Requires-Python: <3.13,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: astropy<8,>=5.3
Requires-Dist: matplotlib<4,>=3.7
Requires-Dist: numpy<2,>=1.23
Requires-Dist: pandas<3,>=1.5
Requires-Dist: scipy<2,>=1.9
Provides-Extra: catalogs
Requires-Dist: astroquery>=0.4.6; extra == "catalogs"
Requires-Dist: barycorrpy>=0.4.4; extra == "catalogs"
Provides-Extra: legacy-barycentric
Requires-Dist: barycorrpy>=0.4.4; extra == "legacy-barycentric"
Provides-Extra: hdf5
Requires-Dist: h5py>=3.8; extra == "hdf5"

# neidspec

`neidspec` provides a spectrum class and supporting utilities for reduced
NEID Level 2 spectra. It is the low-level spectrum dependency used by
NEIDSpecMatch.

## Installation

Use Python 3.10, 3.11, or 3.12:

```bash
python -m pip install neidspec
```

Version 0.2 uses a tested pure-NumPy cross-correlation implementation. The
legacy `crosscorr` package and its Fortran/`numpy.distutils` build are no
longer required.

## Validation boundary

Version 0.2 validates the NEID Level 2 reader, DRP metadata/DQ handling,
order mapping, deblazing, resampling, dynamic rotational broadening, and the
numerical structure of explicit custom-mask CCF fits.  These checks do not by
themselves validate a stellar-parameter scale, a particular line mask, or
compatibility between different DRP reductions.  Publication-facing parameter
inference must add a matched reference-library validation product, as
NEIDSpecMatch does in its strict mode.

Historical general-purpose modules such as `spec_help`, `stats_help`, `priors`,
and plotting helpers remain importable for compatibility but are outside this
release's publication-validation boundary. The unrelated HPF-only
`CreateTemplate` helper was removed and must not be used to construct a
NEIDSpecMatch reference library.

## Reading a spectrum

```python
from neidspec import NEIDSpectrum

with NEIDSpectrum("neidL2_YYYYMMDDThhmmss.fits") as spectrum:
    print(spectrum.drp_version, spectrum.bjd, spectrum.berv)
    flux, error = spectrum.resample_order(wavelength_grid, order=102)
```

The default behavior is deliberately tied to the NEID DRP product:

- Inputs must identify themselves as NEID science, Level 2, high-resolution
  products (`INSTRUME=NEID`, `OBSTYPE=Sci`, `DATALVL=2`, `OBS-MODE=HR`);
  other or ambiguous products are rejected.
- `DQLEVEL1` and `DQLEVEL2` are interpreted using the DRP-defined low two
  assessment bits. Pass (`0`) is accepted; warning (`1`) is accepted with a
  Python warning and marked unvalidated; fail (`2`) and reserved/invalid (`3`)
  are rejected. Missing flags in older products are warned and unvalidated.
  Use `require_dq_pass=True` to reject warning or missing assessments. Higher
  diagnostic bits are preserved and do not by themselves fail an exposure.
- BJD_TDB and barycentric velocity are read from per-order `SSBJDnnn` and
  `SSBRVnnn` header values. They are not recomputed.
- Science and sky flux are deblazed with the corresponding Level 2
  `SCIBLAZE` and `SKYBLAZE` HDUs. Variance is propagated through the same
  division.
- The stellar rest-frame shift uses the Level 2 weighted `CCFRVMOD` value only
  after checking that it is finite, interior to the stored CCF grid, and
  consistent with `CCFRVSUM`. This is structural validation, not proof that
  the DRP RV is astrophysically correct. A failed check raises an actionable
  error; it never silently substitutes a generic-mask CCF.
- Reading a spectrum performs no TIC/SIMBAD query and writes no target cache.
- Default unit telluric/sky error factors do not read external line masks.
  Non-unit mask inflation requires an explicit user-supplied mask path; legacy
  masks with unresolved redistribution rights are not included in artifacts.
  Where telluric and sky masks overlap, the larger factor is applied once (a
  union), rather than multiplying the two factors.
- `spectrum.drp_version`, `data_level`, `observing_mode`, `observatory`, and
  `provenance` retain reduction metadata for downstream products.

An old external instrument-response file can still be used for controlled
comparisons, but it is never selected implicitly:

```python
spectrum = NEIDSpectrum(
    filename,
    blaze_source="legacy_response",
    legacy_response_path="/path/to/response.fits",
)
```

Optional error inflation around a reviewed external mask is explicit as well:

```python
spectrum = NEIDSpectrum(
    filename,
    tell_err_factor=2.0,
    telluric_mask_path="/path/to/telluric-mask.txt",
)
```

An explicitly supplied RV takes precedence over the DRP value:

```python
spectrum = NEIDSpectrum(filename, rv=-12.345)
```

For a custom binary-mask measurement, both the intent and mask must be
explicit (`rv_source="custom_ccf"`, `ccf_mask_path=...`). Because NEID L2
wavelengths are vacuum wavelengths, an unregistered mask must also declare
`ccf_mask_medium="air"` or `"vacuum"`. The exact bundled ESO ESPRESSO M3
bytes (SHA-256 `a3ef5906...bed3`) have a checksum-bound `air` policy and are
converted to vacuum before correlation. A renamed byte-for-byte copy is still
recognized by its digest; a modified file is unregistered and needs an
explicit declaration. This avoids the roughly 83 km/s zero-point error
produced by correlating those air coordinates directly against a NEID vacuum
grid. The bundled mask remains available for reproducibility tests only, and
no science-facing method selects or validates it as a general NEID mask.
Direct `calculate_ccf_for_orders` and `rvabs_for_orders` calls therefore
require an explicit `mask_path` or a provenance-bearing `Mask` object; use
`mask_wavelength_medium` for an unregistered path. Custom-mask fits use
bounded parameters, reject edge/degenerate fits and inconsistent order RVs,
and record the mask SHA-256, input and CCF wavelength media, conversion policy,
and per-order diagnostics. These numerical checks do not validate whether the
mask is astrophysically appropriate for the target.

For direct `Mask` construction, the legacy `use_airtovac=True` or `False`
keyword remains accepted as an explicit medium declaration. Omitting both
that keyword and `wavelength_medium` is accepted only for a checksum-registered
mask; it now raises for an unregistered file rather than silently assuming
vacuum. An explicit declaration that conflicts with a registered digest also
raises.

`NEIDSpecList` accepts either a scalar RV or one value per input file. Use
`None` for files that should use their validated DRP RV, and a catalog value
for spectra whose DRP CCF diagnostics fail:

```python
with NEIDSpecList(filelist=files, rv=[None, -0.7434, None]) as spectra:
    ...
```

Lists created from `filelist` own and close their spectra. A caller-supplied
`splist` is borrowed and is not closed unless `take_ownership=True` is explicit.
`NEIDSpecList.resample_order(..., order=102)` uses the same absolute DRP
array-row order convention as `NEIDSpectrum.resample_order` and forwards
resampling options such as `return_mask` and `max_gap_factor` to every member.

## Resampling and rotational broadening

`resample_order` propagates diagonal variance with squared linear-interpolation
weights and never interpolates across a bad pixel, non-finite wavelength, or a
gap larger than `max_gap_factor` times the typical source spacing. Unsupported
output pixels are NaN; request the mask with `return_mask=True`. Interpolation
makes neighboring outputs correlated, especially on an oversampled grid. The
covariance is not returned, and this limitation plus the oversampling ratio are
recorded in `spectrum.last_resample_provenance`.

Rotational broadening uses a dynamically sized, complete kernel on a uniform
log-wavelength grid. `vsini=0` is an exact identity. A half-kernel at every
order edge or masked gap is trimmed to NaN so edge lines are never reflected or
extrapolated into a fit. Optimizers must freeze a common pixel set at their
maximum allowed rotation using
`neidspec.rotbroad_help.broadening_valid_mask`; otherwise the valid-pixel count
would change with trial `vsini`. `broaden_variance` applies squared weights at
each stage as a diagonal approximation; intermediate covariance is omitted and
the result must not be treated as an independent-pixel likelihood.

## Order identifiers

NEID arrays contain 122 rows. Array row 0 corresponds to physical echelle
order 173, and row 121 corresponds to echelle order 52. NEIDSpecMatch's
familiar orders 55 and 102 are **array-row indices**, not echelle numbers.

```python
from neidspec import order_index_to_echelle

assert order_index_to_echelle(55) == 118
```

`OrderMap` is used throughout the package to translate requested DRP row
indices into sliced in-memory arrays and to construct order-specific header
keywords. Out-of-range requests raise an explicit error.

## Reproducibility guidance

Record `spectrum.drp_version` with derived results. Do not combine spectra or
reference libraries reduced with different NEID DRP minor versions without a
specific validation. `neidspec` records the version; it does not silently
declare mixed reductions scientifically equivalent.

## Optional target resolution

`NEIDSpectrum(..., resolve_target=True)` retains the legacy catalog-resolution
convenience and may query remote services. It is not needed for NEID
barycentric metadata. Generic barycentric recomputation through `Target`
requires an observatory to be supplied explicitly. Named aliases are restricted
to `NEID`, `KPNO`, `WIYN`, and `WIYN 3.5m`, using coordinates recorded in NEID
Level 2 headers; other sites must provide latitude, longitude, and elevation
explicitly, and unknown names raise instead of falling back to geocentric
coordinates. No McDonald Observatory or APO default remains. Catalog RVs
retained by `Target` in km/s are converted to the m/s units required by
`barycorrpy`.

## Tests

The regression suite uses only synthetic FITS fixtures:

```bash
python -m unittest discover -s tests -v
```

Third-party algorithm attribution is recorded in
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
