Skip to content

Frontiers and why

A Study screens every PRE × POST pair four ways. Each screening keeps the pairs that no other pair beats on every one of its objectives (all minimised) and keeps one witness for every pair that was beaten. No objective is weighted against another.

NameV-id aliasObjectivesSize
"angles"V1_ANGLESΔ incidence, Δ emission, Δ phase13
"angles+resolution"V2_ANGLES_RESOLUTIONthe angles and the worse resolution56
"angles+time"V3_ANGLES_TIMEthe angles and both gaps to the event133
"all"V4_FULL_AVAILABLEall six342

The sizes are for the Vikram run of 14 October 2026. An unknown name raises KeyError that lists the valid ones.

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="2023-08-23T12:33Z/12:35Z",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> [len(study.frontier(name)) for name in ("angles", "angles+resolution", "angles+time", "all")]
[13, 56, 133, 342]
>>> [p.pair_id for p in study.frontier("V1_ANGLES")] == [p.pair_id for p in study.frontier("angles")]
True

A Frontier is a read-only sequence of PairView. It also carries what the screening did:

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> f = study.frontier("angles")
>>> f.objectives
('delta_incidence_deg', 'delta_emission_deg', 'delta_phase_deg')
>>> f.comparisons, len(f.witnesses)
(79896, 2327)

comparisons counts the dominance tests. witnesses holds one record per beaten pair: 2,340 pairs, 13 on the frontier, 2,327 beaten.

study.pair(pair_id) returns a PairView: both observation rows, the three angle differences in degrees as deltas (Δsun, Δview, Δphase) and the seconds from each observation to the event as gaps. An unknown id raises KeyError.

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> p = study.pair("pair:1ca27f6bd456b92d")
>>> p.pre["ref"].endswith("nac.m1371614436lc"), p.post["ref"].endswith("nac.m1447694547lc")
(True, True)
>>> tuple(round(x, 2) for x in p.deltas)
(0.0, 0.72, 0.98)
>>> round(p.gaps[0] / 86400), round(p.gaps[1] / 86400, 1)
(878, 3.0)

This pair is 878 days before the event and 3.0 days after it. It is on all four frontiers.

study.why(pair_id, frontier="angles") says whether the pair is on the frontier and, if not, which pair beat it. The witness is the one the record retained. It is not searched for again.

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> why = study.why("pair:002ea7a82e1060f1")
>>> why.on_frontier, why.witness
(False, 'pair:0033128b0d75e56c')
>>> tuple(round(x, 2) for x in why.deltas), tuple(round(x, 2) for x in why.witness_deltas)
((6.79, 0.83, 3.14), (2.78, 0.29, 2.67))
>>> print(why)
pair:002ea7a82e1060f1 is off the angles frontier: pair:0033128b0d75e56c is at least as good on every angle (Δsun 2.78° vs 6.79°, Δview 0.29° vs 0.83°, Δphase 2.67° vs 3.14°) and strictly better on one.

why.everywhere says, for every frontier at once, whether the pair is on it:

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> study.why("pair:1ca27f6bd456b92d").everywhere
{'angles': True, 'angles+resolution': True, 'angles+time': True, 'all': True}

Ask about another frontier by name. The sentence then lists that frontier’s objectives instead of the three angles:

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> print(study.why("pair:002ea7a82e1060f1", "angles+resolution"))
pair:002ea7a82e1060f1 is off the angles+resolution frontier: pair:0033128b0d75e56c is at least as good on every objective (Δsun 2.78° vs 6.79°, Δview 0.29° vs 0.83°, Δphase 2.67° vs 3.14°, resolution 1.68 m/px vs 1.84 m/px) and strictly better on one.

A pair is on the frontier if and only if it has no witness. A witness is at least as good as the pair on every objective and strictly better on at least one.

The event basis sets the two time gaps. In this run the four registered bases of Vikram give the same four frontiers, pair for pair, because no product in the query was observed between 30 July 2023 and 26 August 2023. Their digests differ, because the event basis is part of the record.

>>> import lunartrace as lt
>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> day = lt.temporal.study(site="vikram", event="2023-08-23T00:00Z/2023-08-24T00:00Z",
... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")
>>> day.digest[:15], study.digest[:15]
('sha256:ca4f8ab3', 'sha256:3d75a662')
>>> {n: [p.pair_id for p in day.frontier(n)] == [p.pair_id for p in study.frontier(n)]
... for n in ("angles", "all")}
{'angles': True, 'all': True}