@holoscript/radio-astronomy-plugin 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -0
  3. package/dist/bridge/python-runner.d.ts +24 -0
  4. package/dist/bridge/python-runner.d.ts.map +1 -0
  5. package/dist/bridge/python-runner.js +43 -0
  6. package/dist/bridge/python-runner.js.map +1 -0
  7. package/dist/components/SpectralCubeViewer.d.ts +45 -0
  8. package/dist/components/SpectralCubeViewer.d.ts.map +1 -0
  9. package/dist/components/SpectralCubeViewer.js +212 -0
  10. package/dist/components/SpectralCubeViewer.js.map +1 -0
  11. package/dist/constants/astronomy-traits.d.ts +6 -0
  12. package/dist/constants/astronomy-traits.d.ts.map +1 -0
  13. package/dist/constants/astronomy-traits.js +12 -0
  14. package/dist/constants/astronomy-traits.js.map +1 -0
  15. package/dist/fits/FITSParser.d.ts +60 -0
  16. package/dist/fits/FITSParser.d.ts.map +1 -0
  17. package/dist/fits/FITSParser.js +234 -0
  18. package/dist/fits/FITSParser.js.map +1 -0
  19. package/dist/fits/FITSToGrid.d.ts +27 -0
  20. package/dist/fits/FITSToGrid.d.ts.map +1 -0
  21. package/dist/fits/FITSToGrid.js +85 -0
  22. package/dist/fits/FITSToGrid.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +28 -0
  26. package/dist/index.js.map +1 -0
  27. package/package.json +17 -7
  28. package/plugin.manifest.json +9 -0
  29. package/CHANGELOG.md +0 -19
  30. package/__tests__/SpectralCubeViewer.test.ts +0 -131
  31. package/__tests__/plugin.test.ts +0 -21
  32. package/python/astropy_bridge.py +0 -45
  33. package/src/bridge/python-runner.ts +0 -62
  34. package/src/components/SpectralCubeViewer.tsx +0 -335
  35. package/src/constants/astronomy-traits.ts +0 -13
  36. package/src/fits/FITSParser.ts +0 -312
  37. package/src/fits/FITSToGrid.ts +0 -96
  38. package/src/index.ts +0 -37
  39. package/tsconfig.json +0 -26
  40. package/vitest.config.ts +0 -10
@@ -1,312 +0,0 @@
1
- /**
2
- * FITSParser — Pure JavaScript parser for FITS (Flexible Image Transport System) files.
3
- *
4
- * FITS is the standard data format in astronomy. Structure:
5
- * - Header: 2880-byte blocks of 80-character ASCII "cards" (key=value pairs)
6
- * - Data: big-endian binary arrays, padded to 2880-byte boundary
7
- * - Extensions: additional HDUs (Header Data Units) with same structure
8
- *
9
- * Supports: BITPIX 8 (uint8), 16 (int16), 32 (int32), -32 (float32), -64 (float64)
10
- * Handles: BSCALE/BZERO physical value scaling, WCS coordinate metadata
11
- *
12
- * @see https://fits.gsfc.nasa.gov/fits_standard.html
13
- */
14
-
15
- // ── Types ────────────────────────────────────────────────────────────────────
16
-
17
- export interface WCSInfo {
18
- /** Reference pixel (1-indexed, per FITS convention) */
19
- crpix: number[];
20
- /** Reference value (world coordinate at reference pixel) */
21
- crval: number[];
22
- /** Pixel scale (coordinate increment per pixel) */
23
- cdelt: number[];
24
- /** Axis types (e.g., 'RA---TAN', 'DEC--TAN', 'FREQ') */
25
- ctype: string[];
26
- /** Axis units */
27
- cunit: string[];
28
- }
29
-
30
- export interface FITSFile {
31
- /** All header cards as key→value */
32
- headers: Map<string, string | number | boolean>;
33
- /** Data array (physical values after BSCALE/BZERO) */
34
- data: Float32Array;
35
- /** Axis dimensions [NAXIS1, NAXIS2, ...] */
36
- shape: number[];
37
- /** World Coordinate System info (if present) */
38
- wcs: WCSInfo | null;
39
- /** BITPIX from header */
40
- bitpix: number;
41
- /** Object name (OBJECT card) */
42
- object: string;
43
- /** Telescope name (TELESCOP card) */
44
- telescope: string;
45
- /** Observation date (DATE-OBS card) */
46
- dateObs: string;
47
- }
48
-
49
- // ── Constants ────────────────────────────────────────────────────────────────
50
-
51
- const BLOCK_SIZE = 2880;
52
- const CARD_SIZE = 80;
53
- const CARDS_PER_BLOCK = BLOCK_SIZE / CARD_SIZE; // 36
54
-
55
- // ── Parser ───────────────────────────────────────────────────────────────────
56
-
57
- /**
58
- * Parse a FITS file from an ArrayBuffer.
59
- * Returns the primary HDU (first header + data unit).
60
- */
61
- export function parseFITS(buffer: ArrayBuffer): FITSFile {
62
- const bytes = new Uint8Array(buffer);
63
- const view = new DataView(buffer);
64
-
65
- // ── Parse Header ─────────────────────────────────────────────────
66
- const headers = new Map<string, string | number | boolean>();
67
- let headerEnd = 0;
68
-
69
- outer: for (let block = 0; block * BLOCK_SIZE < buffer.byteLength; block++) {
70
- for (let card = 0; card < CARDS_PER_BLOCK; card++) {
71
- const offset = block * BLOCK_SIZE + card * CARD_SIZE;
72
- if (offset + CARD_SIZE > buffer.byteLength) break outer;
73
-
74
- const cardStr = decodeASCII(bytes, offset, CARD_SIZE);
75
- const keyword = cardStr.substring(0, 8).trim();
76
-
77
- if (keyword === 'END') {
78
- headerEnd = (block + 1) * BLOCK_SIZE; // data starts at next block boundary
79
- break outer;
80
- }
81
-
82
- if (cardStr[8] === '=' && cardStr[9] === ' ') {
83
- const valueStr = cardStr.substring(10).split('/')[0].trim();
84
- headers.set(keyword, parseCardValue(valueStr));
85
- } else if (keyword === 'COMMENT' || keyword === 'HISTORY') {
86
- // Skip comment/history cards
87
- }
88
- }
89
- }
90
-
91
- if (headerEnd === 0) {
92
- throw new Error('FITS: No END card found in header');
93
- }
94
-
95
- // ── Extract Critical Keywords ────────────────────────────────────
96
- const bitpix = getNum(headers, 'BITPIX');
97
- const naxis = getNum(headers, 'NAXIS');
98
-
99
- const shape: number[] = [];
100
- for (let i = 1; i <= naxis; i++) {
101
- shape.push(getNum(headers, `NAXIS${i}`));
102
- }
103
-
104
- const bscale = getNumOr(headers, 'BSCALE', 1.0);
105
- const bzero = getNumOr(headers, 'BZERO', 0.0);
106
-
107
- // ── Parse Data ───────────────────────────────────────────────────
108
- const totalPixels = shape.reduce((a, b) => a * b, 1);
109
- const bytesPerPixel = Math.abs(bitpix) / 8;
110
- const dataOffset = headerEnd;
111
-
112
- if (dataOffset + totalPixels * bytesPerPixel > buffer.byteLength) {
113
- throw new Error(
114
- `FITS: Data section extends beyond buffer (need ${dataOffset + totalPixels * bytesPerPixel}, have ${buffer.byteLength})`
115
- );
116
- }
117
-
118
- const data = new Float32Array(totalPixels);
119
-
120
- for (let i = 0; i < totalPixels; i++) {
121
- const off = dataOffset + i * bytesPerPixel;
122
- let raw: number;
123
-
124
- switch (bitpix) {
125
- case 8:
126
- raw = bytes[off];
127
- break;
128
- case 16:
129
- raw = view.getInt16(off, false); // big-endian
130
- break;
131
- case 32:
132
- raw = view.getInt32(off, false);
133
- break;
134
- case -32:
135
- raw = view.getFloat32(off, false);
136
- break;
137
- case -64:
138
- raw = view.getFloat64(off, false);
139
- break;
140
- default:
141
- throw new Error(`FITS: Unsupported BITPIX ${bitpix}`);
142
- }
143
-
144
- // Apply physical value transformation
145
- data[i] = bscale * raw + bzero;
146
- }
147
-
148
- // ── Extract WCS ──────────────────────────────────────────────────
149
- let wcs: WCSInfo | null = null;
150
- if (headers.has('CRVAL1')) {
151
- wcs = {
152
- crpix: [],
153
- crval: [],
154
- cdelt: [],
155
- ctype: [],
156
- cunit: [],
157
- };
158
- for (let i = 1; i <= naxis; i++) {
159
- wcs.crpix.push(getNumOr(headers, `CRPIX${i}`, 1));
160
- wcs.crval.push(getNumOr(headers, `CRVAL${i}`, 0));
161
- wcs.cdelt.push(getNumOr(headers, `CDELT${i}`, 1));
162
- wcs.ctype.push(getStrOr(headers, `CTYPE${i}`, ''));
163
- wcs.cunit.push(getStrOr(headers, `CUNIT${i}`, ''));
164
- }
165
- }
166
-
167
- return {
168
- headers,
169
- data,
170
- shape,
171
- wcs,
172
- bitpix,
173
- object: getStrOr(headers, 'OBJECT', ''),
174
- telescope: getStrOr(headers, 'TELESCOP', ''),
175
- dateObs: getStrOr(headers, 'DATE-OBS', ''),
176
- };
177
- }
178
-
179
- // ── FITS Builder (for tests) ─────────────────────────────────────────────────
180
-
181
- /**
182
- * Build a minimal FITS file as ArrayBuffer (for testing).
183
- */
184
- export function buildFITS(opts: {
185
- bitpix: number;
186
- shape: number[];
187
- data: number[];
188
- bscale?: number;
189
- bzero?: number;
190
- headers?: Record<string, string | number>;
191
- }): ArrayBuffer {
192
- const cards: string[] = [];
193
-
194
- cards.push(fmtCard('SIMPLE', true));
195
- cards.push(fmtCard('BITPIX', opts.bitpix));
196
- cards.push(fmtCard('NAXIS', opts.shape.length));
197
- for (let i = 0; i < opts.shape.length; i++) {
198
- cards.push(fmtCard(`NAXIS${i + 1}`, opts.shape[i]));
199
- }
200
- if (opts.bscale !== undefined) cards.push(fmtCard('BSCALE', opts.bscale));
201
- if (opts.bzero !== undefined) cards.push(fmtCard('BZERO', opts.bzero));
202
-
203
- if (opts.headers) {
204
- for (const [key, val] of Object.entries(opts.headers)) {
205
- cards.push(fmtCard(key, val));
206
- }
207
- }
208
-
209
- cards.push('END'.padEnd(CARD_SIZE));
210
-
211
- // Pad header to block boundary
212
- while (cards.length % CARDS_PER_BLOCK !== 0) {
213
- cards.push(' '.repeat(CARD_SIZE));
214
- }
215
-
216
- const headerBytes = new Uint8Array(cards.length * CARD_SIZE);
217
- for (let i = 0; i < cards.length; i++) {
218
- for (let j = 0; j < CARD_SIZE; j++) {
219
- headerBytes[i * CARD_SIZE + j] = cards[i].charCodeAt(j);
220
- }
221
- }
222
-
223
- // Write data
224
- const bytesPerPixel = Math.abs(opts.bitpix) / 8;
225
- const dataSize = opts.data.length * bytesPerPixel;
226
- const paddedDataSize = Math.ceil(dataSize / BLOCK_SIZE) * BLOCK_SIZE;
227
- const dataBytes = new ArrayBuffer(paddedDataSize);
228
- const dataView = new DataView(dataBytes);
229
-
230
- for (let i = 0; i < opts.data.length; i++) {
231
- const off = i * bytesPerPixel;
232
- switch (opts.bitpix) {
233
- case 8:
234
- new Uint8Array(dataBytes)[off] = opts.data[i];
235
- break;
236
- case 16:
237
- dataView.setInt16(off, opts.data[i], false);
238
- break;
239
- case 32:
240
- dataView.setInt32(off, opts.data[i], false);
241
- break;
242
- case -32:
243
- dataView.setFloat32(off, opts.data[i], false);
244
- break;
245
- case -64:
246
- dataView.setFloat64(off, opts.data[i], false);
247
- break;
248
- }
249
- }
250
-
251
- // Combine
252
- const result = new Uint8Array(headerBytes.length + paddedDataSize);
253
- result.set(headerBytes);
254
- result.set(new Uint8Array(dataBytes), headerBytes.length);
255
- return result.buffer;
256
- }
257
-
258
- // ── Helpers ──────────────────────────────────────────────────────────────────
259
-
260
- function decodeASCII(bytes: Uint8Array, offset: number, length: number): string {
261
- let s = '';
262
- for (let i = 0; i < length; i++) {
263
- s += String.fromCharCode(bytes[offset + i]);
264
- }
265
- return s;
266
- }
267
-
268
- function parseCardValue(s: string): string | number | boolean {
269
- if (s === 'T') return true;
270
- if (s === 'F') return false;
271
- if (s.startsWith("'")) return s.replace(/^'|'$/g, '').trim();
272
- const n = Number(s);
273
- return Number.isNaN(n) ? s : n;
274
- }
275
-
276
- function getNum(headers: Map<string, string | number | boolean>, key: string): number {
277
- const v = headers.get(key);
278
- if (typeof v !== 'number') throw new Error(`FITS: Missing or non-numeric header ${key}`);
279
- return v;
280
- }
281
-
282
- function getNumOr(
283
- headers: Map<string, string | number | boolean>,
284
- key: string,
285
- def: number
286
- ): number {
287
- const v = headers.get(key);
288
- return typeof v === 'number' ? v : def;
289
- }
290
-
291
- function getStrOr(
292
- headers: Map<string, string | number | boolean>,
293
- key: string,
294
- def: string
295
- ): string {
296
- const v = headers.get(key);
297
- return typeof v === 'string' ? v : def;
298
- }
299
-
300
- function fmtCard(keyword: string, value: string | number | boolean): string {
301
- const kw = keyword.padEnd(8);
302
- let valStr: string;
303
- if (typeof value === 'boolean') {
304
- valStr = value ? 'T' : 'F';
305
- valStr = valStr.padStart(20);
306
- } else if (typeof value === 'number') {
307
- valStr = String(value).padStart(20);
308
- } else {
309
- valStr = `'${value}'`.padEnd(20);
310
- }
311
- return `${kw}= ${valStr}`.padEnd(CARD_SIZE);
312
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * FITSToGrid — Convert parsed FITS data to HoloScript grid structures.
3
- *
4
- * Maps FITS spectral cubes (RA × Dec × Freq) and 2D images to
5
- * RegularGrid3D for visualization via ScalarFieldOverlay or SimResultsMesh.
6
- */
7
-
8
- import { RegularGrid3D } from '@holoscript/engine/simulation';
9
- import type { FITSFile } from './FITSParser';
10
-
11
- /**
12
- * Convert a parsed FITS file to a RegularGrid3D.
13
- *
14
- * - 3D cubes (NAXIS=3): maps directly to grid
15
- * - 2D images (NAXIS=2): creates a 1-deep grid (nx × ny × 1)
16
- * - 1D spectra (NAXIS=1): creates a 1×1×n grid
17
- */
18
- export function fitsToGrid3D(fits: FITSFile): RegularGrid3D {
19
- const shape = fits.shape;
20
-
21
- let nx: number, ny: number, nz: number;
22
-
23
- if (shape.length >= 3) {
24
- nx = shape[0];
25
- ny = shape[1];
26
- nz = shape[2];
27
- } else if (shape.length === 2) {
28
- nx = shape[0];
29
- ny = shape[1];
30
- nz = 1;
31
- } else if (shape.length === 1) {
32
- nx = shape[0];
33
- ny = 1;
34
- nz = 1;
35
- } else {
36
- throw new Error(`FITS: Cannot convert ${shape.length}D data to grid`);
37
- }
38
-
39
- const grid = new RegularGrid3D([nx, ny, nz], [nx, ny, nz]);
40
-
41
- // FITS stores data in FORTRAN order (column-major: NAXIS1 varies fastest)
42
- // RegularGrid3D uses row-major (x varies fastest in our convention)
43
- // Since NAXIS1 = x and it varies fastest in both, direct copy works
44
- const data = fits.data;
45
- const gridData = grid.data;
46
- const len = Math.min(data.length, gridData.length);
47
-
48
- for (let i = 0; i < len; i++) {
49
- gridData[i] = data[i];
50
- }
51
-
52
- return grid;
53
- }
54
-
55
- /**
56
- * Extract a single frequency channel (z-slice) from a 3D FITS cube.
57
- * Returns a 2D Float32Array (nx × ny) for use as a ScalarFieldOverlay.
58
- */
59
- export function extractChannel(fits: FITSFile, channel: number): Float32Array {
60
- if (fits.shape.length < 3) {
61
- // 2D image — return the whole thing
62
- return new Float32Array(fits.data);
63
- }
64
-
65
- const nx = fits.shape[0];
66
- const ny = fits.shape[1];
67
- const nz = fits.shape[2];
68
-
69
- if (channel < 0 || channel >= nz) {
70
- throw new Error(`Channel ${channel} out of range [0, ${nz - 1}]`);
71
- }
72
-
73
- const slice = new Float32Array(nx * ny);
74
- const offset = channel * nx * ny;
75
-
76
- for (let i = 0; i < nx * ny; i++) {
77
- slice[i] = fits.data[offset + i];
78
- }
79
-
80
- return slice;
81
- }
82
-
83
- /**
84
- * Get data range (min/max) for the FITS data.
85
- * Useful for colormap normalization.
86
- */
87
- export function fitsDataRange(fits: FITSFile): [number, number] {
88
- let min = Infinity,
89
- max = -Infinity;
90
- for (let i = 0; i < fits.data.length; i++) {
91
- const v = fits.data[i];
92
- if (v < min) min = v;
93
- if (v > max) max = v;
94
- }
95
- return [min, max];
96
- }
package/src/index.ts DELETED
@@ -1,37 +0,0 @@
1
- import { RADIO_ASTRONOMY_TRAITS, RadioAstronomyTraitName } from './constants/astronomy-traits';
2
- import { PythonAstropyBridge, AstropyResult } from './bridge/python-runner';
3
-
4
- /**
5
- * @holoscript/radio-astronomy-plugin
6
- *
7
- * Domain plugin bridging Radio Astrophysics simulation concepts into the HoloScript Universal pipeline.
8
- * Extends standard traits without bloating core. Provides an astropy python bridge for logic evaluation.
9
- */
10
-
11
- // Export vocabulary
12
- export { RADIO_ASTRONOMY_TRAITS, type RadioAstronomyTraitName };
13
-
14
- // Export Bridges
15
- export { PythonAstropyBridge, type AstropyResult };
16
-
17
- // Export FITS parsing and visualization
18
- export { parseFITS, buildFITS, type FITSFile, type WCSInfo } from './fits/FITSParser';
19
- export { fitsToGrid3D, extractChannel, fitsDataRange } from './fits/FITSToGrid';
20
- export {
21
- SpectralCubeViewer,
22
- FITSViewerPanel,
23
- type SpectralCubeViewerProps,
24
- type FITSViewerPanelProps,
25
- } from './components/SpectralCubeViewer';
26
-
27
- /**
28
- * Metadata exposing domain capabilities to the Studio / Schema Mapper.
29
- */
30
- export const DOMAIN_MANIFEST = {
31
- id: 'domain.science.astronomy.radio',
32
- name: 'Radio Astronomy Plugin',
33
- version: '1.0.0',
34
- description: 'Extends HoloScript spatial environments with radio astrophysics primitives.',
35
- keywords: ['interferometer', 'radio emitting', 'synchrotron radiation', 'pulsar'],
36
- traits: RADIO_ASTRONOMY_TRAITS,
37
- };
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2020", "DOM"],
7
- "declaration": true,
8
- "declarationMap": true,
9
- "sourceMap": true,
10
- "strict": true,
11
- "noImplicitAny": true,
12
- "strictNullChecks": true,
13
- "noUnusedLocals": false,
14
- "noUnusedParameters": false,
15
- "resolveJsonModule": true,
16
- "esModuleInterop": true,
17
- "skipLibCheck": true,
18
- "forceConsistentCasingInFileNames": true,
19
- "outDir": "./dist",
20
- "rootDir": "./src",
21
- "experimentalDecorators": true,
22
- "jsx": "react-jsx"
23
- },
24
- "include": ["src/**/*"],
25
- "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.bench.ts", "src/__tests__/**/*"]
26
- }
package/vitest.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: 'node',
7
- include: ['src/**/*.test.ts'],
8
- passWithNoTests: true,
9
- },
10
- });