Skip to content

Commit d367caf

Browse files
committed
add new-instrument/SKILL.md
1 parent 95b47ca commit d367caf

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
---
2+
name: new-instrument
3+
description: Set up a new instrument in PyReduce with config.yaml, settings.json, __init__.py, and example script
4+
---
5+
6+
# Adding a New Instrument to PyReduce
7+
8+
## Overview
9+
10+
Each instrument lives in `pyreduce/instruments/{NAME}/` (uppercase) and needs at minimum three files plus an example script.
11+
12+
## Step-by-step
13+
14+
### 1. Inspect the FITS data
15+
16+
Read headers from one file of each type (bias, flat, wavecal, science) to determine:
17+
18+
- **Detector dimensions**: NAXIS1 x NAXIS2
19+
- **Gain keyword or value**: e.g. `DETGAIN`, or literal like `1.2`
20+
- **Readnoise keyword or value**: may not exist in header, use known value
21+
- **Date keyword**: usually `DATE-OBS`
22+
- **Target keyword**: usually `OBJECT`
23+
- **Instrument keyword**: usually `INSTRUME`, note the value for `id_instrument`
24+
- **File classification keyword**: find which header keyword distinguishes bias/flat/wavecal/science (e.g. `OBSMODE`, `EXPTYPE`, `ESO DPR TYPE`)
25+
- **Classification values**: the values of that keyword for each file type
26+
- **Coordinates**: RA/DEC keywords, observatory lon/lat/alt
27+
28+
To find the **overscan/prescan** regions, compare a flat field with the known active CCD area:
29+
- Look at column means: prescan columns have bias-level values even in flats
30+
- Look at row means: overscan rows drop to bias level at the detector edge
31+
- Confirm: `prescan_x + active_pixels + overscan_x = NAXIS1` (and similarly for y)
32+
33+
### 2. Determine orientation
34+
35+
The **orientation** code rotates the raw image so dispersion runs along x. Try to figure out which raw axis is longer (dispersion) and which is shorter (cross-dispersion). Common orientations:
36+
- `0`: no change
37+
- `1`: 90 deg CCW
38+
- `3`: 270 deg CCW (common when NAXIS2 > NAXIS1 is the dispersion axis)
39+
- `4`: transpose
40+
41+
If uncertain, pick the most likely value and verify with a trace run. Traces should appear as roughly horizontal lines.
42+
43+
### 3. Create `pyreduce/instruments/{NAME}/config.yaml`
44+
45+
Required fields:
46+
47+
```yaml
48+
__instrument__: NAME
49+
instrument: INSTRUME # header keyword containing instrument name
50+
id_instrument: NAME # value to match in that keyword
51+
telescope: TelescopeName
52+
53+
date: DATE-OBS
54+
date_format: fits
55+
56+
extension: 0 # FITS extension with image data
57+
orientation: 3 # rotation code
58+
transpose: false
59+
60+
prescan_x: 53 # pixels to trim from raw NAXIS1 start
61+
overscan_x: 47 # pixels to trim from raw NAXIS1 end
62+
prescan_y: 0
63+
overscan_y: 64
64+
naxis_x: NAXIS1
65+
naxis_y: NAXIS2
66+
67+
gain: DETGAIN # header keyword or literal number
68+
readnoise: 5.5 # header keyword or literal number
69+
dark: 0
70+
sky: 0
71+
exposure_time: EXPTIME
72+
73+
ra: OBJ_RA # or RA, etc
74+
dec: OBJ_DEC # or DEC, etc
75+
longitude: -17.8792 # observatory coordinates (literal)
76+
latitude: 28.7603
77+
altitude: 2333
78+
target: OBJECT
79+
observation_type: OBSMODE # header keyword for file type
80+
81+
# File classification: all kw_ fields use the same header keyword
82+
# id_ fields are regex patterns to match against that keyword
83+
kw_bias: OBSMODE
84+
kw_flat: OBSMODE
85+
kw_curvature: OBSMODE
86+
kw_scatter: OBSMODE
87+
kw_orders: OBSMODE
88+
kw_wave: OBSMODE
89+
kw_comb: null
90+
kw_spec: OBSMODE
91+
92+
id_bias: BIAS
93+
id_flat: HRF_FF
94+
id_orders: HRF_FF # same files used for order tracing
95+
id_curvature: HRF_TH # or same as flat
96+
id_scatter: HRF_FF
97+
id_wave: HRF_TH
98+
id_comb: null
99+
id_spec: HRF_OBJ
100+
```
101+
102+
**Multi-fiber instruments**: Add a `fibers:` section if the instrument has multiple fibers per order.
103+
104+
```yaml
105+
fibers:
106+
fibers_per_order: 2 # auto-pairs traces by gap analysis
107+
groups:
108+
obj:
109+
range: [1, 2] # half-open: fiber_idx 1 only
110+
merge: center
111+
sky:
112+
range: [2, 3] # half-open: fiber_idx 2 only
113+
merge: center
114+
use:
115+
default: [obj, sky] # extract each group separately
116+
```
117+
118+
Key points about fiber config:
119+
- `range: [start, end]` is **half-open** (1-based). `[1, 2]` = fiber 1 only, `[2, 3]` = fiber 2 only
120+
- `merge: center` picks the center fiber; `merge: average` averages; `merge: [1]` picks the 1st fiber in the range (1-indexed within range)
121+
- `use.default: [obj, sky]` makes each group extract independently. Without this, all grouped traces are mixed together which causes problems when the extraction height calculation needs overlapping column ranges between neighbors
122+
- Without `use`, the default is `"groups"` which concatenates all grouped traces into one list -- this breaks extraction for dual-fiber instruments because neighboring traces (obj/sky of same order) are only ~6px apart and edge orders may have non-overlapping column ranges
123+
124+
### 4. Create `pyreduce/instruments/{NAME}/settings.json`
125+
126+
Inherit defaults and override what's needed:
127+
128+
```json
129+
{
130+
"__instrument__": "NAME",
131+
"__inherits__": "defaults/settings.json",
132+
"trace": {
133+
"degree": 6,
134+
"noise": 50,
135+
"min_cluster": 3000,
136+
"filter_y": 120
137+
},
138+
"norm_flat": {
139+
"extraction_height": 6,
140+
"oversampling": 10
141+
},
142+
"wavecal_master": {
143+
"extraction_height": 6
144+
},
145+
"science": {
146+
"extraction_height": 6,
147+
"oversampling": 10
148+
}
149+
}
150+
```
151+
152+
**Important**: For multi-fiber instruments, set `extraction_height` to an explicit pixel value (>= 2) in norm_flat, wavecal_master, and science. A fractional value like `0.5` triggers neighbor-distance calculation which fails when edge orders have non-overlapping column ranges. The pixel value should be less than half the inter-order separation between same-group traces.
153+
154+
### 5. Create `pyreduce/instruments/{NAME}/__init__.py`
155+
156+
Minimal version:
157+
158+
```python
159+
"""Instrument-specific info for {NAME}."""
160+
161+
import logging
162+
163+
from ..common import Instrument
164+
165+
logger = logging.getLogger(__name__)
166+
167+
168+
class NAME(Instrument):
169+
def add_header_info(self, header, channel, **kwargs):
170+
header = super().add_header_info(header, channel)
171+
return header
172+
```
173+
174+
The class name **must** match the directory name (uppercase). Override methods only if needed (e.g. RA unit conversion, JD offset, custom wavecal filename).
175+
176+
### 6. Create `examples/{name}_example.py`
177+
178+
Use `Pipeline.from_instrument()` for instruments with proper config:
179+
180+
```python
181+
from pyreduce.pipeline import Pipeline
182+
import os
183+
184+
base_dir = os.environ.get("REDUCE_DATA", os.path.expanduser("~/REDUCE_DATA"))
185+
186+
data = Pipeline.from_instrument(
187+
instrument="NAME",
188+
target="TargetName",
189+
night="2026-04-12",
190+
channel="",
191+
steps=("bias", "flat", "trace", "norm_flat", "wavecal_master", "science"),
192+
base_dir=base_dir,
193+
input_dir=os.path.join(base_dir, "NAME"),
194+
plot=1,
195+
).run()
196+
```
197+
198+
### 7. Verify
199+
200+
Test incrementally:
201+
1. `load_instrument('NAME')` -- config loads without errors
202+
2. `sort_files(...)` -- files classified correctly
203+
3. Run `bias, flat, trace` -- check trace count and positions
204+
4. Run `norm_flat` -- extraction succeeds
205+
5. Run `wavecal_master` -- extraction succeeds
206+
6. Run `science` -- extraction succeeds
207+
208+
Use `PYREDUCE_PLOT=0` for headless testing.
209+
210+
## Common pitfalls
211+
212+
- **Orientation wrong**: traces appear vertical or diagonal instead of horizontal. Try different orientation codes.
213+
- **Prescan/overscan wrong**: images have bright/dark strips at edges. Check flat field column/row means to identify the transition from bias level to signal.
214+
- **File classification misses files**: check that `kw_*` points to the right header keyword and `id_*` patterns match (case-insensitive regex).
215+
- **Edge orders cause extraction failures**: partial orders at detector edges have short column ranges that don't overlap with neighbors. Fix by using explicit pixel extraction heights (>= 2) instead of fractional values.
216+
- **Multi-fiber extraction crashes**: grouped traces from different fibers are interleaved with small separations (~6px). Always set `use.default: [group1, group2]` to extract groups independently.
217+
- **"No instrument channels found"**: harmless warning when `channels` is not set in config. Only needed for instruments with separate detector chips/arms.
218+
219+
## Optional additions
220+
221+
- `order_centers_{channel}.yaml`: Known y-positions for order matching (assigns physical order numbers `m`)
222+
- `wavecal_*.npz`: Pre-computed wavelength solutions
223+
- `mask_*.fits.gz`: Bad pixel masks
224+
- `settings_{channel}.json`: Per-channel setting overrides (uses `"__inherits__": "{NAME}/settings.json"`)

0 commit comments

Comments
 (0)