Smithsonian / Smithsonian/layup

IOD is confined to seq[0], which is too thin on sparse multi-night data

Open
#587 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
10
Forks
2
Avg merge
1d 43m
Merged PRs (30d)
58

Description

do_fit runs IOD on the primary segment alone (orbitfit.py:1181):

obs = [observations[i] for i in seq[0]]

_build_sequence splits on 90-day gaps, so when a survey reports one observation per night and the nights are months apart, every segment is one night. Both IOD paths then close:

  • Gauss. _select_gauss_triplet returns None below three observations, and the fallback (iod.py:15) builds (0, 1, 1) from a two-element segment -- a repeated index, unguarded. The C++ binding returns None for it, so nothing worse happens, but the fallback is constructing an input it knows is invalid and relying on the callee to reject it.
  • BK-IOD. The fallback is gated on len(obs) >= 3 against that same segment (orbitfit.py:1206), so it never runs.

With no candidates, do_fit returns flag = 5, stage = 1.

Measured

The 110 trans-Neptunian objects of Smotherman et al. 2024 (AJ 167, 136), from a DEEP shift-and-stack search, detected on four or five nights over two years, fitted through the streak path (iod="auto", engine="bk_native", one streak observation per night):

len(seq[0]) objects outcome
2 102 flag 5, every one
3 8 1 flag 0, 6 flag 4, 1 exceeded a 120 s timeout

The separation is exact: every object whose primary segment holds two observations fails with no solution, and no object whose segment holds three does. In point form -- the same detections, two per night -- len(seq[0]) is 4 or 6 and never below 3.

run_bk_iod on the full observation set converges for 110 of 110, on genuine streak observations as well as on the point form. So the seed the fallback would need is available and good; the len(obs) >= 3 gate against seq[0] is what prevents it being asked for.

One object, D458636, exceeds 120 s under per-object isolation against roughly five seconds for the rest. Separately, a single long-lived process fitting these in sequence stalled at 100 per cent CPU for 40 minutes on one object before being killed; per-object forks did not reproduce it on that object, so it may be accumulated state rather than the object.

Suggested change
  1. When the primary segment cannot support an IOD, widen the input rather than declining: run BK-IOD on the full observation set before returning FLAG_NO_SOLUTION. The measurement above says the seed is there for all 110.
  2. Guard the Gauss fallback: if _select_gauss_triplet returns None and len(idx0) < 3, return no candidates instead of building a triplet with a repeated index. Testable without any fitting.
Related
  • #556 -- same seq[0], the screen rather than the IOD input.
  • #586 -- BK-IOD reachable only as a fallback after every Gauss root fails, never by request.

Drafted with Claude Opus 5.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in orbitfit.py at do_fit around lines 1181 and 1206, then inspect the Gauss fallback in iod.py around line 15. Reproduce a sparse multi-night case and trace the primary segment versus the full observation set. Done means the full set can reach BK-IOD when the segment is insufficient, and invalid short Gauss inputs produce no candidates; add regression coverage for both paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
backend, data
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.