-
Notifications
You must be signed in to change notification settings - Fork 0
Add example UDL federation plugin (ephemeris + conjunction assessments) #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
62df42f
29cb665
5a0bdcb
f4d22f7
a0e7ac1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /***************************************************************************** | ||
| * Open MCT, Copyright (c) 2014-2024, United States Government | ||
| * as represented by the Administrator of the National Aeronautics and Space | ||
| * Administration. All rights reserved. | ||
| * | ||
| * Open MCT is licensed under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations | ||
| * under the License. | ||
| * | ||
| * Open MCT includes source code licensed under additional open source | ||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||
| * this source code distribution or the Licensing information page available | ||
| * at runtime from the About dialog for additional information. | ||
| *****************************************************************************/ | ||
|
|
||
| // Screening thresholds commonly used for conjunction assessment triage. | ||
| const PC_CRITICAL = 1e-4; | ||
| const PC_WARNING = 1e-5; | ||
|
|
||
| const LIMITS = { | ||
| critical: { | ||
| cssClass: 'is-limit--upr is-limit--red', | ||
| low: PC_CRITICAL, | ||
| high: Number.POSITIVE_INFINITY, | ||
| name: 'Pc Critical' | ||
| }, | ||
| warning: { | ||
| cssClass: 'is-limit--upr is-limit--yellow', | ||
| low: PC_WARNING, | ||
| high: PC_CRITICAL, | ||
| name: 'Pc Warning' | ||
| } | ||
| }; | ||
|
|
||
| export default class ConjunctionLimitProvider { | ||
| supportsLimits(domainObject) { | ||
| return domainObject.type === 'udl.conjunctions'; | ||
| } | ||
|
|
||
| getLimitEvaluator() { | ||
| return { | ||
| evaluate: function (datum, valueMetadata) { | ||
| if (!valueMetadata || valueMetadata.key !== 'pc') { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (datum.pc >= PC_CRITICAL) { | ||
| return LIMITS.critical; | ||
| } | ||
|
|
||
| if (datum.pc >= PC_WARNING) { | ||
| return LIMITS.warning; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| getLimits() { | ||
| return { | ||
| limits: function () { | ||
| return Promise.resolve({ | ||
| WARNING: { | ||
| high: { | ||
| color: 'yellow', | ||
| pc: PC_WARNING | ||
| } | ||
| }, | ||
| CRITICAL: { | ||
| high: { | ||
| color: 'red', | ||
| pc: PC_CRITICAL | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| } | ||
|
Comment on lines
+67
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Limit provider interface and getLimits format match existing conventions I verified Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
+67
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Pc limit lines render a bogus value if a non-pc range is plotted
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /***************************************************************************** | ||
| * Open MCT, Copyright (c) 2014-2024, United States Government | ||
| * as represented by the Administrator of the National Aeronautics and Space | ||
| * Administration. All rights reserved. | ||
| * | ||
| * Open MCT is licensed under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations | ||
| * under the License. | ||
| * | ||
| * Open MCT includes source code licensed under additional open source | ||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||
| * this source code distribution or the Licensing information page available | ||
| * at runtime from the About dialog for additional information. | ||
| *****************************************************************************/ | ||
|
|
||
| import { SATELLITES } from './satellites.js'; | ||
|
|
||
| const EPHEMERIS_VALUES = [ | ||
| { | ||
| key: 'utc', | ||
| source: 'utc', | ||
| name: 'Timestamp', | ||
| format: 'utc', | ||
| hints: { domain: 1 } | ||
| }, | ||
| { | ||
| key: 'altitude', | ||
| name: 'Altitude', | ||
| unit: 'km', | ||
| formatString: '%0.2f', | ||
| hints: { range: 1 } | ||
| }, | ||
| { | ||
| key: 'latitude', | ||
| name: 'Latitude', | ||
| unit: 'deg', | ||
| formatString: '%0.4f', | ||
| hints: { range: 2 } | ||
| }, | ||
| { | ||
| key: 'longitude', | ||
| name: 'Longitude', | ||
| unit: 'deg', | ||
| formatString: '%0.4f', | ||
| hints: { range: 3 } | ||
| }, | ||
| { | ||
| key: 'velocity', | ||
| name: 'Velocity', | ||
| unit: 'km/s', | ||
| formatString: '%0.3f', | ||
| hints: { range: 4 } | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| ]; | ||
|
|
||
| const CONJUNCTION_VALUES = [ | ||
| { | ||
| key: 'utc', | ||
| source: 'utc', | ||
| name: 'Timestamp', | ||
| format: 'utc', | ||
| hints: { domain: 1 } | ||
| }, | ||
| { | ||
| key: 'pc', | ||
| name: 'Probability of Collision', | ||
| hints: { range: 1 } | ||
| }, | ||
| { | ||
| key: 'missDistance', | ||
| name: 'Miss Distance', | ||
| unit: 'km', | ||
| formatString: '%0.2f', | ||
| hints: { range: 2 } | ||
| }, | ||
| { | ||
| key: 'secondary', | ||
| name: 'Secondary Object', | ||
| format: 'string' | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| ]; | ||
|
|
||
| export default class UDLObjectProvider { | ||
| get(identifier) { | ||
| if (identifier.key === 'node') { | ||
| return Promise.resolve({ | ||
| identifier, | ||
| name: 'UDL Federation Node', | ||
| type: 'folder', | ||
| location: 'ROOT' | ||
| }); | ||
| } | ||
|
|
||
| if (identifier.key === 'conjunctions') { | ||
| return Promise.resolve({ | ||
| identifier, | ||
| name: 'Conjunction Assessments', | ||
| type: 'udl.conjunctions', | ||
| location: 'udl:node', | ||
| telemetry: { | ||
| values: CONJUNCTION_VALUES | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const satellite = SATELLITES.find((candidate) => candidate.key === identifier.key); | ||
| if (satellite === undefined) { | ||
| return Promise.reject(new Error(`Unknown UDL object: ${identifier.key}`)); | ||
| } | ||
|
|
||
| return Promise.resolve({ | ||
| identifier, | ||
| name: satellite.name, | ||
| type: 'udl.ephemeris', | ||
| location: 'udl:node', | ||
| noradId: satellite.noradId, | ||
| telemetry: { | ||
| values: EPHEMERIS_VALUES | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /***************************************************************************** | ||
| * Open MCT, Copyright (c) 2014-2024, United States Government | ||
| * as represented by the Administrator of the National Aeronautics and Space | ||
| * Administration. All rights reserved. | ||
| * | ||
| * Open MCT is licensed under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations | ||
| * under the License. | ||
| * | ||
| * Open MCT includes source code licensed under additional open source | ||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||
| * this source code distribution or the Licensing information page available | ||
| * at runtime from the About dialog for additional information. | ||
| *****************************************************************************/ | ||
|
|
||
| import { EARTH_RADIUS_KM, MU_EARTH, SATELLITES } from './satellites.js'; | ||
|
|
||
| const EPHEMERIS_PERIOD_MS = 1000; | ||
| const CONJUNCTION_PERIOD_MS = 5000; | ||
| const MAX_REQUEST_DATUMS = 50000; | ||
| const SECONDARY_OBJECTS = [ | ||
| 'COSMOS 2251 DEB', | ||
| 'FENGYUN 1C DEB', | ||
| 'SL-16 R/B', | ||
| 'STARLINK-30452', | ||
| 'IRIDIUM 33 DEB' | ||
| ]; | ||
|
|
||
| function ephemerisDatum(satellite, timestamp) { | ||
| const radiusKm = EARTH_RADIUS_KM + satellite.altitudeKm; | ||
| const periodSeconds = 2 * Math.PI * Math.sqrt(Math.pow(radiusKm, 3) / MU_EARTH); | ||
| const meanMotion = (2 * Math.PI) / periodSeconds; | ||
| const theta = meanMotion * (timestamp / 1000) + (satellite.phaseDeg * Math.PI) / 180; | ||
| const inclination = (satellite.inclinationDeg * Math.PI) / 180; | ||
|
|
||
| const latitude = (Math.asin(Math.sin(inclination) * Math.sin(theta)) * 180) / Math.PI; | ||
| const earthRotationDeg = ((timestamp / 1000) * 360) / 86164; | ||
| const longitude = | ||
| (((((Math.atan2(Math.cos(inclination) * Math.sin(theta), Math.cos(theta)) * 180) / Math.PI + | ||
| satellite.raanDeg - | ||
| earthRotationDeg) % | ||
| 360) + | ||
| 540) % | ||
| 360) - | ||
| 180; | ||
|
|
||
| return { | ||
| id: satellite.key, | ||
| utc: timestamp, | ||
| altitude: satellite.altitudeKm + Math.sin(theta * 3) * 2, | ||
| latitude, | ||
| longitude, | ||
| velocity: Math.sqrt(MU_EARTH / radiusKm) | ||
| }; | ||
| } | ||
|
|
||
| function conjunctionDatum(timestamp) { | ||
| const slot = Math.floor(timestamp / CONJUNCTION_PERIOD_MS); | ||
| const wave = Math.abs(Math.sin(slot / 7)); | ||
| const pc = wave > 0.92 ? 0.0002 + wave / 2000 : wave / 50000; | ||
|
|
||
| return { | ||
| id: 'conjunctions', | ||
| utc: timestamp, | ||
| pc: Number(pc.toPrecision(3)), | ||
| missDistance: Number((0.4 + (1 - wave) * 24).toFixed(2)), | ||
| secondary: SECONDARY_OBJECTS[slot % SECONDARY_OBJECTS.length] | ||
| }; | ||
| } | ||
|
|
||
| export default class UDLTelemetryProvider { | ||
| supportsRequest(domainObject) { | ||
| return this.#isUDLTelemetry(domainObject); | ||
| } | ||
|
|
||
| supportsSubscribe(domainObject) { | ||
| return this.#isUDLTelemetry(domainObject); | ||
| } | ||
|
|
||
| request(domainObject, options) { | ||
| const period = | ||
| domainObject.type === 'udl.conjunctions' ? CONJUNCTION_PERIOD_MS : EPHEMERIS_PERIOD_MS; | ||
| const size = Math.min(options.size ?? MAX_REQUEST_DATUMS, MAX_REQUEST_DATUMS); | ||
| const start = Math.floor(options.start / period) * period; | ||
| const requestedCount = Math.floor((options.end - start) / period) + 1; | ||
| const count = Math.min(requestedCount, size); | ||
| const data = []; | ||
|
|
||
| if (options.strategy === 'latest') { | ||
| const alignedEnd = Math.floor(options.end / period) * period; | ||
| for (let i = count - 1; i >= 0; i--) { | ||
| data.push(this.#datumFor(domainObject, alignedEnd - i * period)); | ||
| } | ||
| } else { | ||
| const step = count > 1 ? (options.end - start) / (count - 1) : period; | ||
| for (let i = 0; i < count; i++) { | ||
| data.push(this.#datumFor(domainObject, start + Math.round(i * step))); | ||
| } | ||
| } | ||
|
Comment on lines
+101
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Historical request spaces datums evenly rather than at true cadence when capped In Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| return Promise.resolve(data); | ||
| } | ||
|
Comment on lines
+87
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Ephemeris view could generate up to 50000 full-resolution datums per request When no Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed — |
||
|
|
||
| subscribe(domainObject, callback) { | ||
| const period = | ||
| domainObject.type === 'udl.conjunctions' ? CONJUNCTION_PERIOD_MS : EPHEMERIS_PERIOD_MS; | ||
| const interval = setInterval(() => { | ||
| callback(this.#datumFor(domainObject, Date.now())); | ||
| }, period); | ||
|
|
||
| return function unsubscribe() { | ||
| clearInterval(interval); | ||
| }; | ||
| } | ||
|
Comment on lines
+111
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Realtime datum timestamps are unaligned versus aligned historical timestamps
Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — as noted, conjunction datum content is slot-keyed and ephemeris is a continuous function of timestamp, so the unaligned live-edge timestamps stay consistent with historical values. Leaving as-is. |
||
|
|
||
| #datumFor(domainObject, timestamp) { | ||
| if (domainObject.type === 'udl.conjunctions') { | ||
| return conjunctionDatum(timestamp); | ||
| } | ||
|
|
||
| const satellite = SATELLITES.find((candidate) => candidate.key === domainObject.identifier.key); | ||
|
|
||
| return ephemerisDatum(satellite, timestamp); | ||
| } | ||
|
Comment on lines
+123
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Ephemeris datum lookup assumes the satellite key always exists
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| #isUDLTelemetry(domainObject) { | ||
| return domainObject.type === 'udl.ephemeris' || domainObject.type === 'udl.conjunctions'; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: cspell relies on 3-letter identifiers being under minWordLength
The PR adds many domain terms to
.cspell.json(elset, statevector, NAVSTAR, raan, sbirs, FENGYUN, STARLINK, norad) but not 'udl', 'gps', 'geo', 'wgs', 'deb', which appear widely in the new code. This passes CI only because cspell's default minWordLength (4) skips those 3-char tokens, and multi-char terms like 'ephemeris'/'conjunction'/'iridium'/'cosmos' are standard dictionary words. If the spellcheck config or dictionary set changes, these could start failing. Not flagged as a bug since the current CI check passes.Was this helpful? React with 👍 or 👎 to provide feedback.