Space / frontend /src /lib /conjWorker.js
Harp404's picture
update2
a07741b
Raw History Blame Contribute Delete
5.53 kB
// Conjunction-screening Web Worker.
// Pulls the full catalogue (3LE) from the gateway, then for a set of PRIMARY
// objects screens against all others for close approaches over a time window:
// apogee–perigee sieve → coarse SGP4 screen → fine TCA refine → Pc.
// Runs off the main thread so the globe stays smooth. Posts progress + results.
import * as satellite from 'satellite.js'
const MU = 398600.4418 // km^3/s^2
const RE = 6378.137 // km
function orbitParams(rec) {
const nRadSec = rec.no / 60
const a = Math.cbrt(MU / (nRadSec * nRadSec))
const e = rec.ecco
return { peri: a * (1 - e) - RE, apo: a * (1 + e) - RE }
}
// Screening-grade probability of collision (2-D circular Gaussian, Foster-style).
// Honest: TLEs carry no covariance, so we assume a 1-sigma positional uncertainty
// and a combined hard-body radius. This is a SCREENING estimate, not operational.
function probCollision(missKm) {
const sigma = 3.0 // km, combined 1-sigma — TLE error grows to a few km over a day
const hbr = 0.03 // km, combined hard-body radius (~30 m)
const pc = (hbr * hbr) / (2 * sigma * sigma) * Math.exp(-(missKm * missKm) / (2 * sigma * sigma))
return Math.min(pc, 1)
}
self.onmessage = async (ev) => {
const {
primaries, // array of NORAD ids to screen (the "watch list")
startMs = Date.now(),
hours = 24,
coarseSec = 240,
gateKm = 25, // apogee-perigee altitude gate
candidateKm = 10, // keep approaches closer than this
maxResults = 12,
} = ev.data
// 1) Fetch + parse the catalogue.
let txt = ''
try {
const res = await fetch('/api/catalogue')
txt = await res.text()
} catch (e) {
self.postMessage({ error: 'catalogue fetch failed: ' + e.message }); return
}
const lines = txt.split('\n').map((l) => l.trimEnd())
const objs = []
for (let i = 0; i + 2 < lines.length; i += 3) {
let name = lines[i]; if (name.startsWith('0 ')) name = name.slice(2).trim()
const l1 = lines[i + 1], l2 = lines[i + 2]
if (!l1.startsWith('1 ') || !l2.startsWith('2 ')) continue
const norad = parseInt(l2.substring(2, 7)); if (!norad) continue
try {
const rec = satellite.twoline2satrec(l1, l2)
if (!rec || rec.error) continue
objs.push({ norad, name, rec, ...orbitParams(rec) })
} catch { /* skip */ }
}
// 2) Candidate pairs via apogee-perigee sieve.
const primarySet = new Set(primaries)
const prim = objs.filter((o) => primarySet.has(o.norad))
const pairs = []
for (const A of prim) {
for (const B of objs) {
if (A.norad >= B.norad) continue // unordered, no self
if (A.peri > B.apo + gateKm || B.peri > A.apo + gateKm) continue
pairs.push([A, B])
}
}
// 3) Coarse screen — propagate only the involved objects once per step (cached).
const involved = new Map()
for (const [A, B] of pairs) { involved.set(A.norad, A); involved.set(B.norad, B) }
const inv = [...involved.values()]
const steps = Math.ceil((hours * 3600) / coarseSec)
const best = new Map()
for (let s = 0; s <= steps; s++) {
const d = new Date(startMs + s * coarseSec * 1000)
for (const o of inv) { const pv = satellite.propagate(o.rec, d); o._p = (pv && pv.position) ? pv.position : null }
for (const [A, B] of pairs) {
if (!A._p || !B._p) continue
const dx = A._p.x - B._p.x, dy = A._p.y - B._p.y, dz = A._p.z - B._p.z
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
const k = A.norad + '-' + B.norad
const c = best.get(k)
if (!c || dist < c.d) best.set(k, { d: dist, t: d.getTime(), A, B })
}
if (s % 20 === 0) self.postMessage({ progress: s / steps, screened: pairs.length, objects: objs.length })
}
// 4) Fine TCA refine for sub-candidate-threshold events, + Pc + relative speed.
const results = []
for (const c of best.values()) {
if (c.d > candidateKm) continue
let bestD = c.d, bestT = c.t
for (let tt = c.t - coarseSec * 1000; tt <= c.t + coarseSec * 1000; tt += 2000) {
const d = new Date(tt)
const pa = satellite.propagate(c.A.rec, d), pb = satellite.propagate(c.B.rec, d)
if (!pa.position || !pb.position) continue
const dx = pa.position.x - pb.position.x, dy = pa.position.y - pb.position.y, dz = pa.position.z - pb.position.z
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
if (dist < bestD) { bestD = dist; bestT = tt }
}
// relative speed over 1 s at TCA
const d1 = new Date(bestT), d2 = new Date(bestT + 1000)
const a1 = satellite.propagate(c.A.rec, d1), b1 = satellite.propagate(c.B.rec, d1)
const a2 = satellite.propagate(c.A.rec, d2), b2 = satellite.propagate(c.B.rec, d2)
let relV = 0
if (a1.position && b1.position && a2.position && b2.position) {
const r1x = a1.position.x - b1.position.x, r1y = a1.position.y - b1.position.y, r1z = a1.position.z - b1.position.z
const r2x = a2.position.x - b2.position.x, r2y = a2.position.y - b2.position.y, r2z = a2.position.z - b2.position.z
relV = Math.sqrt((r2x - r1x) ** 2 + (r2y - r1y) ** 2 + (r2z - r1z) ** 2)
}
results.push({
a: c.A.norad, aName: c.A.name, b: c.B.norad, bName: c.B.name,
tcaMs: bestT, missKm: bestD, relVelKmS: relV, pc: probCollision(bestD),
})
}
results.sort((x, y) => x.missKm - y.missKm)
self.postMessage({ conjunctions: results.slice(0, maxResults), screened: pairs.length, objects: objs.length, done: true })
}