Skip to content

Enhance field of view with interpolated masks and geometry helpers - #52

Merged
thkruz merged 24 commits into
mainfrom
develop
Oct 27, 2026
Merged

thkruz merged 24 commits into
mainfrom
develop

Conversation

@thkruz

@thkruz thkruz commented Oct 26, 2026

Copy link
Copy Markdown
Owner

This pull request adds a type-checking step for integration tests to the continuous integration workflow. This ensures that type errors in integration tests are caught earlier in the CI process.

  • CI workflow improvements:
    • Added a Type-check integration tests step to .github/workflows/ci.yml to run pnpm run typecheck:integration before executing integration tests.

thkruz added 24 commits October 12, 2026 10:08
FieldOfViewParams gains an optional `interpolate` flag (default false, so
existing step-sector masks and catalog JSON keep today's behaviour). When
true, each ElevationMask entry is a vertex at (startAz, minEl) joined
linearly to the next vertex, wrapping at 360 deg; stopAz is ignored. This
matches SODA's piecewise-linear horizon mask semantics.

- Vertices are sorted once in the constructor; getMinElevation does a
  binary search and never sorts or allocates.
- New pure maskMinElevation(masks, az, interpolate, floorEl) is the single
  implementation, shared with KeepTrack's legacy sensor path.
- Effective floor is max(minElevation, mask(az)).
- Validation for interpolated masks: 2 to 72 vertices, 0 <= az < 360,
  0 <= el <= 89, unique azimuths; throws ValidationError.
- serialize() and toString() carry the flag.
The local eslint/tsup DTS failures were a stale node_modules. After
rm -rf node_modules && pnpm install --frozen-lockfile, eslint and the
tsup DTS build both pass unchanged, so eslint stays (no biome switch).

- tsconfig.base.json and test/integration/tsconfig.json: types
  ["jest", "node"] -> ["node", "vitest/globals"]. 157 of 164 test files
  use describe/it/expect as globals (vitest globals: true), so the
  vitest/globals types are required.
- Remove @types/jest from devDependencies and the lockfile.
- Fix the 12 tsc errors in src unit tests that surfaced: jest fail()
  -> expect.fail(), missing required name on TransmitterParams /
  ReceiverParams, zero-valued Degrees casts on Radians parameters,
  mockImplementation() arity, Vector3D unit casts.
- tsconfig.json excludes test/integration, which has its own tsconfig
  resolving "ootk" to dist. That project still reports 99 pre-existing
  type errors (unit-brand drift, e.g. Degrees passed to Radians fields);
  not addressed here.

Fresh worktree at this tree (git worktree add <scratch> HEAD):
pnpm install --frozen-lockfile: exit 0, Done in 10.2s
pnpm lint: exit 0, 0 errors, 35 warnings (same as before)
pnpm build: exit 0, ESM/CJS/DTS build success
pnpm exec tsc --noEmit -p tsconfig.json: exit 0, 0 errors
pnpm test: exit 0, 138 passed | 1 skipped files, 2230 passed | 20 skipped
(the skipped file needs the gitignored Sgp4Prop*.wasm binaries)
contains() now checks range, the global minimum elevation, the cone, and
only then the azimuth mask (skipped entirely when there are no masks).
The checks are conjunctive and the mask never drops below minElevation,
so results are unchanged. containsDirection() never consults the mask,
and VisualizationHelpers uses getMinElevation to clamp mesh points, not
as a containment test, so neither needed the reorder.

FieldOfView.mask.test.ts gains a hand-derived boolean table on a masked
cone (outside the cone above the mask, inside the cone below the mask,
below the global floor, on the mask boundary, behind, out of range). It
passes against both the old and the new ordering.

Timed loop, not committed: 72-vertex interpolated mask, cone boresight
az 90 el 30 half-angle 20, 1e6 seeded random RAE points with 90% outside
the cone, 3 warm-up passes, median of 7, tsx on Node 22, three runs each:
before: 92.0 / 92.0 / 94.9 ms
after:  63.4 / 63.1 / 62.2 ms
SwathGeometry gives the per-sample frame (geodetic up, along, right), the
WGS84 local radius and horizon half-angle, swath width to FOV conversion,
ray/ellipsoid hits, horizon-clamped cross-track edges (plus an
allocation-free variant for per-frame use), the sub-point daylight test and
the lit/unlit run splitter with shared boundary samples.
Inventory of `tsc --noEmit -p test/integration/tsconfig.json` against the
built dist before this series (99 errors):

| File                     | Unit | Null | Missing field | Other | Total |
| ------------------------ | ---- | ---- | ------------- | ----- | ----- |
| lambert.test.ts          |   25 |   15 |             0 |     0 |    40 |
| orbital-elements.test.ts |   24 |    0 |             0 |     0 |    24 |
| maneuvers.test.ts        |   12 |    0 |             0 |     0 |    12 |
| doppler.test.ts          |    0 |    6 |             0 |     0 |     6 |
| observations.test.ts     |    0 |    6 |             0 |     0 |     6 |
| sensor-fov.test.ts       |    0 |    0 |             3 |     3 |     6 |
| iod.test.ts              |    0 |    4 |             0 |     0 |     4 |
| moon.test.ts             |    0 |    0 |             0 |     1 |     1 |
| Total                    |   61 |   31 |             3 |     4 |    99 |

Unit: Degrees passed as Radians, plain number passed as Kilometers or
KilometersPerSecond. Null: T | null or T | undefined used unchecked. Missing
field: object literal uses a FieldOfViewParams key that no longer exists.
Other: string id where number is typed (3), instantiating an abstract class (1).

This commit:
- lib/assert.ts: add expectNotNull, a real assertion (not.toBeNull and
  toBeDefined) that narrows T | null. toBeDefined alone passes on null, so the
  old checks on LambertIOD.estimate and friends were vacuous.
- lambert: Vector3D<Kilometers>(n, n, n) becomes Vector3D(n as Kilometers, ...)
  on literals already in km; circular speed sqrt(mu / r1) is km/s by
  construction. estimate() and Satellite.lla() results go through
  expectNotNull. The multi-rev loop keeps its `if (solution)` because nRev 1
  and 2 may have no solution. "should handle close positions" now requires a
  non-null solution (it was toBeDefined, which a null passed).
- iod, doppler: estimate(), applyDoppler() and dopplerFactor() results go
  through expectNotNull.

No expected values changed; all tests in these files pass. Remaining
integration tsc errors after this commit: 49.
- maneuvers, orbital-elements: ClassicalElements takes Radians, but the tests
  passed degree literals branded as Degrees (inclination 28.5, argPerigee 270
  and so on were read as radians). Inputs are now `(n * DEG2RAD) as Radians`;
  zeros are `0 as Radians`. Every assertion in these tests depends only on
  semimajor axis, eccentricity, period, or radius at true anomaly 0, which do
  not depend on the angles, so no expected value changed.
- observations: RadecTopocentric/RadecGeocentric.range is Kilometers |
  undefined and GroundStation.rae() is nullable; both go through
  expectNotNull before use.
- moon: GroundObject is abstract; the observer is now a GroundStation with the
  same lat, lon, alt and name.
- sensor-fov: the PhasedArrayRadar fixtures used the removed min/max azimuth
  and elevation FieldOfView keys and a string id. id is now 1 (typed number).
  The old window (az 0-360, el 10-85) becomes the hemisphere form of the
  current cone API: halfAngle 80 about the zenith boresight default with
  minElevation 10. The 85 deg upper limit has no cone equivalent and is
  dropped. Before this, halfAngle was undefined at runtime, so the cone was
  NaN; the assertions (minElevation 10, maxRange 5556, canObserve returns a
  boolean, getRae is defined) are unchanged and pass.

Previously-wrong tests (expected value changed): none. The unit fixes
changed inputs that no assertion reads.

TODO(api) casts: none. RadecGeocentric.fromDegrees (rates typed Degrees) is
not called by any integration test.

Integration tsc errors: 99 before this series, 0 after.
- package.json: add `typecheck` (tsc -p tsconfig.json) and
  `typecheck:integration` (tsc -p test/integration/tsconfig.json, which
  resolves ootk to dist). `test:integration` now runs build, then
  typecheck:integration, then vitest.
- ci.yml: the test-integration job (the one that downloads the dist
  artifact) runs typecheck:integration before test:integration:only.

Gates, fresh worktree at this commit (pnpm install --frozen-lockfile first):
pnpm lint: exit 0 (0 errors, 35 warnings)
pnpm build: exit 0
pnpm exec tsc --noEmit -p tsconfig.json: exit 0
pnpm typecheck:integration: exit 0
pnpm test: exit 0 (2255 passed, 20 skipped of 2276)
pnpm test:integration:only: exit 0 (291 passed, 3 skipped of 294)

`pnpm test` has a pre-existing intermittent failure under the full parallel
run (a vitest forks worker exits unexpectedly, or
ConjunctionAssessment.highfidelity throws "y2.add is not a function" in
RungeKuttaAdaptive). It reproduces at f4eaa92 (1 of 3 runs) and the file
passes 3 of 3 runs alone. Not addressed here.
Vitest defaults to one fork per logical core, which is 31 workers on the
32-thread Windows dev box. That initial burst can exceed what Windows will
commit, and forks die intermittently with V8 "Zone Allocation failed -
process out of memory". An intermittent ootk worker crash matched this; a
30-run investigation found no cause in the code itself.

Cap maxWorkers at min(12, availableParallelism - 1) in both the unit and
integration configs (the integration config does not inherit the root
one). This is the same cap the keeptrack-space parent applies in its
vitest.config.ts for the same crash. CI runners (2-4 cores) are unaffected.
Fixes the silently wrong numbers found in the 2026-10-13 QA deep dive across operations, time, transforms, covariance, sgp4, objects, orbit determination, conjunction, sensor and parsers, and moves the qa proof tests into the module __tests__ folders as permanent regression tests.

BREAKING CHANGES: mag2db is renamed pow2db and Lambert posigrade is renamed shortWay (no aliases); cappedScreeningCovarianceFromTle returns RIC variances in R, I, C order and its caps are sigma caps; chanPc2D is corrected and the secondary covariance is rotated into the primary RIC frame; estimateRcs returns m2 and estimateVmagFromRcs uses the corrected constant (1 m2 is about +7.9); at boresight elevation 90 the roll angle is measured from the boresight azimuth (FieldOfView.migrateZenithRollFromV7 converts v7 configs); fovFromSwathWidth returns NaN past the horizon; Satellite and Tle apogee/perigee are altitudes above 6378.137 km; sccNum5/sccNum6 keep leading zeros and sccNum6 is 6 wide; CDM parse/export is in metres; ScreeningFilter takes a shell pad in km (useInclinationFilter and shellsOverlapWithInclination removed); Matrix.inverse throws SingularMatrixError; FieldOfView requires halfAngle; Beacon.getNextTransmission returns the next window only. See CHANGELOG for the migration section.
…act snapshots

The pre-push suite failed on a 4e-12 km last-ulp difference in driftDegreesToSma: x ** (1 / 3) is not bit-stable across V8 JIT tiers. Math.cbrt is, and the three tests now check to 1 mm instead of snapshotting every digit.
…al normal integral, analytic Pc bound and absolute error floor

normalIntervalProbability_ lost precision to erfc cancellation on chords much
narrower than sigma, so the 1e-12 relative tolerance was unreachable and the
adaptive rule bisected to depth 30 (one Pc of 2.9e-7 took 259 s). Narrow
intervals now integrate the density with one Kronrod rule; an analytic upper
bound returns 0 below PC_NEGLIGIBLE (1e-30); panels stop at an absolute error
floor of 1e-16 of the running total.
…one in final windows and floor flat-valley sigma
A fixed FOV from one altitude makes the ground width follow the height above
the surface, which changes by about 21 km between the equator and the poles on
a near-circular orbit. This re-solves the look half-angle at each position on
the local WGS84 radius, so a published swath width holds at every latitude.
@thkruz
thkruz merged commit 02a290b into main Oct 27, 2026
7 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 7.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant