@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,45 +0,0 @@
1
- import sys
2
- import json
3
- import math
4
-
5
- def calculate_synchrotron(params):
6
- """
7
- Mock calculation for synchrotron radiation flux based on magnetic field strength.
8
- In a fully operational deployment, this would use `astropy.units` and `astropy.constants`.
9
- """
10
- b_field = params.get('magnetic_field_gauss', 1e-4) # default interstellar field
11
- frequency = params.get('frequency_hz', 1.4e9) # default 1.4 GHz (HI line proximity)
12
-
13
- # Placeholder formula for demonstration (proportionality mock)
14
- # Spectral index alpha ~ 0.75
15
- flux = (b_field ** 2) * (frequency ** -0.75) * 1e12
16
-
17
- return {
18
- "flux_density_jy": flux,
19
- "frequency_hz": frequency,
20
- "wavelength_meters": 3e8 / frequency
21
- }
22
-
23
- def main():
24
- if len(sys.argv) < 3:
25
- print(json.dumps({"error": "Insufficient arguments. Usage: script.py <command> <json_params>"}))
26
- sys.exit(1)
27
-
28
- command = sys.argv[1]
29
-
30
- try:
31
- params = json.loads(sys.argv[2])
32
- except Exception as e:
33
- print(json.dumps({"error": f"Invalid JSON payload: {str(e)}"}))
34
- sys.exit(1)
35
-
36
- if command == "calc_synchrotron":
37
- result = calculate_synchrotron(params)
38
- else:
39
- result = {"error": f"Unknown command: {command}"}
40
-
41
- # Print pure JSON to stdout for the JS bridge to parse
42
- print(json.dumps(result))
43
-
44
- if __name__ == "__main__":
45
- main()
@@ -1,62 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import * as path from 'path';
3
-
4
- /**
5
- * Interface representing the result of a radio astronomy physics calculation.
6
- */
7
- export interface AstropyResult {
8
- wavelength_meters?: number;
9
- frequency_hz?: number;
10
- flux_density_jy?: number;
11
- error?: string;
12
- }
13
-
14
- /**
15
- * Runner that bridges HoloScript to the underlying astropy Python toolkit.
16
- */
17
- export class PythonAstropyBridge {
18
- private scriptPath: string;
19
-
20
- constructor() {
21
- this.scriptPath = path.resolve(__dirname, '../../python/astropy_bridge.py');
22
- }
23
-
24
- /**
25
- * Dispatches a calculation to the Python environment.
26
- * @param command The astronomical command to invoke (e.g. 'calc_synchrotron')
27
- * @param params JSON payload of parameters
28
- * @returns A promise resolving to the astronomical result payload
29
- */
30
- public async executeCommand(
31
- command: string,
32
- params: Record<string, any>
33
- ): Promise<AstropyResult> {
34
- return new Promise((resolve, reject) => {
35
- const process = spawn('python', [this.scriptPath, command, JSON.stringify(params)]);
36
-
37
- let output = '';
38
- let errorOutput = '';
39
-
40
- process.stdout.on('data', (data) => {
41
- output += data.toString();
42
- });
43
-
44
- process.stderr.on('data', (data) => {
45
- errorOutput += data.toString();
46
- });
47
-
48
- process.on('close', (code) => {
49
- if (code !== 0) {
50
- return reject(new Error(`Astropy bridge exited with code ${code}: ${errorOutput}`));
51
- }
52
- try {
53
- // Attempt to parse JSON line output from python script
54
- const parsed = JSON.parse(output.trim());
55
- resolve(parsed as AstropyResult);
56
- } catch (_e) {
57
- reject(new Error(`Failed to parse bridge output: ${output}`));
58
- }
59
- });
60
- });
61
- }
62
- }
@@ -1,335 +0,0 @@
1
- /**
2
- * SpectralCubeViewer — Interactive 3D viewer for FITS spectral cubes.
3
- *
4
- * Drop a FITS file → see the spectral cube in 3D → slide through
5
- * frequency channels → play as animation. Zero code required.
6
- *
7
- * Works with:
8
- * - 3D cubes (RA × Dec × Freq): full volumetric slice-by-slice
9
- * - 2D images (RA × Dec): single-channel colormap
10
- * - 1D spectra: displayed as a line in 3D space
11
- */
12
-
13
- import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
14
- import { useFrame } from '@react-three/fiber';
15
- import * as THREE from 'three';
16
-
17
- // ── Types ────────────────────────────────────────────────────────────────────
18
-
19
- export type ColormapName = 'jet' | 'viridis' | 'turbo' | 'inferno' | 'coolwarm';
20
-
21
- export interface SpectralCubeViewerProps {
22
- /** Parsed FITS data array (physical values after BSCALE/BZERO) */
23
- data: Float32Array;
24
- /** Axis dimensions [NAXIS1, NAXIS2] or [NAXIS1, NAXIS2, NAXIS3] */
25
- shape: number[];
26
- /** Colormap (default: 'viridis') */
27
- colormap?: ColormapName;
28
- /** Auto-play through channels (default: false) */
29
- autoPlay?: boolean;
30
- /** Playback speed in channels per second (default: 5) */
31
- playSpeed?: number;
32
- /** WCS metadata for axis labels */
33
- wcs?: { ctype?: string[]; cunit?: string[]; crval?: number[]; cdelt?: number[] };
34
- /** Object name from FITS header */
35
- objectName?: string;
36
- }
37
-
38
- // ── Colormap GLSL ────────────────────────────────────────────────────────────
39
-
40
- const COLORMAPS: Record<string, string> = {
41
- viridis: `
42
- vec3 colormap(float t) {
43
- vec3 c0 = vec3(0.267, 0.004, 0.329);
44
- vec3 c4 = vec3(0.127, 0.566, 0.551);
45
- vec3 c8 = vec3(0.993, 0.906, 0.144);
46
- if (t < 0.5) return mix(c0, c4, t * 2.0);
47
- return mix(c4, c8, (t - 0.5) * 2.0);
48
- }
49
- `,
50
- turbo: `
51
- vec3 colormap(float t) {
52
- float r = 0.136 + t * (4.615 + t * (-42.66 + t * (132.13 + t * (-152.55 + t * 56.31))));
53
- float g = 0.091 + t * (2.264 + t * (-14.02 + t * (32.21 + t * (-29.27 + t * 10.16))));
54
- float b = 0.107 + t * (12.75 + t * (-60.58 + t * (132.75 + t * (-134.01 + t * 50.26))));
55
- return clamp(vec3(r, g, b), 0.0, 1.0);
56
- }
57
- `,
58
- inferno: `
59
- vec3 colormap(float t) {
60
- vec3 c0 = vec3(0.001, 0.0, 0.014);
61
- vec3 c3 = vec3(0.735, 0.216, 0.329);
62
- vec3 c6 = vec3(0.988, 0.999, 0.644);
63
- if (t < 0.5) return mix(c0, c3, t * 2.0);
64
- return mix(c3, c6, (t - 0.5) * 2.0);
65
- }
66
- `,
67
- };
68
-
69
- const VERT = /* glsl */ `
70
- attribute float aIntensity;
71
- uniform float uMin;
72
- uniform float uMax;
73
- varying float vNorm;
74
-
75
- void main() {
76
- float range = uMax - uMin;
77
- vNorm = range > 0.0 ? clamp((aIntensity - uMin) / range, 0.0, 1.0) : 0.5;
78
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
79
- }
80
- `;
81
-
82
- function makeFrag(cmName: string): string {
83
- const cm = COLORMAPS[cmName] ?? COLORMAPS.viridis;
84
- return /* glsl */ `
85
- varying float vNorm;
86
- ${cm}
87
- void main() {
88
- vec3 c = colormap(vNorm);
89
- gl_FragColor = vec4(c, 1.0);
90
- }
91
- `;
92
- }
93
-
94
- // ── Component ────────────────────────────────────────────────────────────────
95
-
96
- export function SpectralCubeViewer({
97
- data,
98
- shape,
99
- colormap = 'viridis',
100
- autoPlay = false,
101
- playSpeed = 5,
102
- wcs,
103
- objectName,
104
- }: SpectralCubeViewerProps) {
105
- const nx = shape[0] ?? 1;
106
- const ny = shape[1] ?? 1;
107
- const nz = shape[2] ?? 1;
108
- const is3D = shape.length >= 3 && nz > 1;
109
-
110
- const [channel, setChannel] = useState(0);
111
- const [playing, setPlaying] = useState(autoPlay);
112
- const meshRef = useRef<THREE.Mesh>(null);
113
- const timeRef = useRef(0);
114
-
115
- // Extract channel slice
116
- const channelData = useMemo(() => {
117
- if (!is3D) return data; // 2D — use entire dataset
118
- const offset = channel * nx * ny;
119
- return data.slice(offset, offset + nx * ny);
120
- }, [data, channel, nx, ny, is3D]);
121
-
122
- // Data range for colormap normalization
123
- const [min, max] = useMemo(() => {
124
- let lo = Infinity,
125
- hi = -Infinity;
126
- for (let i = 0; i < channelData.length; i++) {
127
- if (channelData[i] < lo) lo = channelData[i];
128
- if (channelData[i] > hi) hi = channelData[i];
129
- }
130
- return [lo, hi];
131
- }, [channelData]);
132
-
133
- // Build geometry: flat plane with per-pixel intensity attribute
134
- const geometry = useMemo(() => {
135
- const geo = new THREE.PlaneGeometry(
136
- nx / Math.max(nx, ny),
137
- ny / Math.max(nx, ny),
138
- nx - 1,
139
- ny - 1
140
- );
141
- const intensities = new Float32Array(nx * ny);
142
- for (let j = 0; j < ny; j++) {
143
- for (let i = 0; i < nx; i++) {
144
- // PlaneGeometry vertex order: row by row, top to bottom
145
- intensities[j * nx + i] = channelData[j * nx + i] ?? 0;
146
- }
147
- }
148
- geo.setAttribute('aIntensity', new THREE.BufferAttribute(intensities, 1));
149
- return geo;
150
- }, [nx, ny, channelData]);
151
-
152
- // Auto-play animation
153
- useFrame((_, delta) => {
154
- if (!playing || !is3D) return;
155
- timeRef.current += delta * playSpeed;
156
- if (timeRef.current >= 1) {
157
- timeRef.current -= 1;
158
- setChannel((c) => (c + 1) % nz);
159
- }
160
- });
161
-
162
- const fragmentShader = useMemo(() => makeFrag(colormap), [colormap]);
163
-
164
- const uniforms = useMemo(
165
- () => ({
166
- uMin: { value: min },
167
- uMax: { value: max },
168
- }),
169
- [min, max]
170
- );
171
-
172
- // Channel label from WCS
173
- const channelLabel = useMemo(() => {
174
- if (!wcs?.crval || !wcs?.cdelt || !wcs?.ctype) return `Channel ${channel}/${nz}`;
175
- const freqIdx = wcs.ctype.findIndex((t) => t.includes('FREQ'));
176
- if (freqIdx < 0) return `Channel ${channel}/${nz}`;
177
- const freq = wcs.crval[freqIdx] + channel * wcs.cdelt[freqIdx];
178
- const unit = wcs.cunit?.[freqIdx] ?? 'Hz';
179
- if (freq >= 1e9) return `${(freq / 1e9).toFixed(3)} GHz`;
180
- if (freq >= 1e6) return `${(freq / 1e6).toFixed(3)} MHz`;
181
- return `${freq.toFixed(0)} ${unit}`;
182
- }, [channel, nz, wcs]);
183
-
184
- return (
185
- <group>
186
- <mesh ref={meshRef} geometry={geometry}>
187
- <shaderMaterial
188
- vertexShader={VERT}
189
- fragmentShader={fragmentShader}
190
- uniforms={uniforms}
191
- side={THREE.DoubleSide}
192
- />
193
- </mesh>
194
-
195
- {/* HUD - rendered as HTML overlay in Studio, or as 3D text in VR */}
196
- {/* For now, the parent component handles the slider UI */}
197
- </group>
198
- );
199
- }
200
-
201
- // ── Studio Panel Wrapper ─────────────────────────────────────────────────────
202
-
203
- export interface FITSViewerPanelProps {
204
- /** Raw FITS ArrayBuffer (from file drop) */
205
- fitsBuffer: ArrayBuffer;
206
- onClose?: () => void;
207
- }
208
-
209
- /**
210
- * Full FITS viewer panel: parses FITS → shows 3D → channel slider.
211
- * This is the entry point for drag-and-drop FITS files.
212
- */
213
- export function FITSViewerPanel({ fitsBuffer, onClose }: FITSViewerPanelProps) {
214
- const [fitsData, setFitsData] = useState<{
215
- data: Float32Array;
216
- shape: number[];
217
- wcs: unknown;
218
- object: string;
219
- } | null>(null);
220
- const [channel, setChannel] = useState(0);
221
- const [playing, setPlaying] = useState(false);
222
- const [error, setError] = useState<string | null>(null);
223
-
224
- useEffect(() => {
225
- try {
226
- // Dynamic import to avoid bundling FITS parser unless needed
227
- import('../fits/FITSParser')
228
- .then(({ parseFITS }) => {
229
- const fits = parseFITS(fitsBuffer);
230
- setFitsData({
231
- data: fits.data,
232
- shape: fits.shape,
233
- wcs: fits.wcs,
234
- object: fits.object || 'Unknown Object',
235
- });
236
- })
237
- .catch((e) => setError(String(e)));
238
- } catch (e) {
239
- setError(String(e));
240
- }
241
- }, [fitsBuffer]);
242
-
243
- if (error) {
244
- return (
245
- <div style={{ padding: 20, color: '#ef4444', fontFamily: 'monospace' }}>
246
- FITS Parse Error: {error}
247
- </div>
248
- );
249
- }
250
-
251
- if (!fitsData) {
252
- return (
253
- <div style={{ padding: 20, color: '#94a3b8', fontFamily: 'monospace' }}>
254
- Loading FITS data...
255
- </div>
256
- );
257
- }
258
-
259
- const nz = fitsData.shape[2] ?? 1;
260
- const is3D = fitsData.shape.length >= 3 && nz > 1;
261
-
262
- return (
263
- <div
264
- style={{
265
- display: 'flex',
266
- flexDirection: 'column',
267
- gap: 8,
268
- padding: 12,
269
- background: '#1a1a2e',
270
- borderRadius: 8,
271
- border: '1px solid #2a2a3e',
272
- color: '#e4e4e7',
273
- fontFamily: "'Space Mono', monospace",
274
- }}
275
- >
276
- {/* Header */}
277
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
278
- <span style={{ fontSize: 13, fontWeight: 700, color: '#a78bfa' }}>{fitsData.object}</span>
279
- <span style={{ fontSize: 10, color: '#71717a' }}>{fitsData.shape.join(' × ')} px</span>
280
- </div>
281
-
282
- {/* Channel Slider (only for 3D cubes) */}
283
- {is3D && (
284
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
285
- <button
286
- onClick={() => setPlaying(!playing)}
287
- style={{
288
- background: playing ? '#ef4444' : '#3b82f6',
289
- border: 'none',
290
- borderRadius: 4,
291
- padding: '4px 10px',
292
- color: 'white',
293
- fontSize: 11,
294
- cursor: 'pointer',
295
- }}
296
- >
297
- {playing ? 'Stop' : 'Play'}
298
- </button>
299
- <input
300
- type="range"
301
- min={0}
302
- max={nz - 1}
303
- value={channel}
304
- onChange={(e) => {
305
- setChannel(Number(e.target.value));
306
- setPlaying(false);
307
- }}
308
- style={{ flex: 1 }}
309
- />
310
- <span style={{ fontSize: 10, color: '#71717a', minWidth: 80, textAlign: 'right' }}>
311
- Ch {channel}/{nz - 1}
312
- </span>
313
- </div>
314
- )}
315
-
316
- {/* Data range */}
317
- <div style={{ fontSize: 10, color: '#71717a' }}>
318
- Range:{' '}
319
- {dataRange(fitsData.data)
320
- .map((v) => v.toExponential(2))
321
- .join(' → ')}
322
- </div>
323
- </div>
324
- );
325
- }
326
-
327
- function dataRange(data: Float32Array): [number, number] {
328
- let min = Infinity,
329
- max = -Infinity;
330
- for (let i = 0; i < data.length; i++) {
331
- if (data[i] < min) min = data[i];
332
- if (data[i] > max) max = data[i];
333
- }
334
- return [min, max];
335
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Specific traits representing properties for Radio Astrophysics.
3
- */
4
- export const RADIO_ASTRONOMY_TRAITS = [
5
- 'radio_emitter', // Signals a 3D construct as an origin point for specific radio wavelengths
6
- 'synchrotron', // Determines emission behavior via magnetic fields and relativistic electrons
7
- 'interferometer', // Marks a multi-nodal virtual sensor
8
- 'em_wave', // Represents an electromagnetic wave primitive
9
- 'pulsar_timing', // Represents pulsar timing array signals
10
- 'spectral_line', // Maps to particular spectral line signals (e.g., 21cm HI line)
11
- ] as const;
12
-
13
- export type RadioAstronomyTraitName = (typeof RADIO_ASTRONOMY_TRAITS)[number];