@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.
- package/LICENSE +21 -0
- package/README.md +14 -0
- package/dist/bridge/python-runner.d.ts +24 -0
- package/dist/bridge/python-runner.d.ts.map +1 -0
- package/dist/bridge/python-runner.js +43 -0
- package/dist/bridge/python-runner.js.map +1 -0
- package/dist/components/SpectralCubeViewer.d.ts +45 -0
- package/dist/components/SpectralCubeViewer.d.ts.map +1 -0
- package/dist/components/SpectralCubeViewer.js +212 -0
- package/dist/components/SpectralCubeViewer.js.map +1 -0
- package/dist/constants/astronomy-traits.d.ts +6 -0
- package/dist/constants/astronomy-traits.d.ts.map +1 -0
- package/dist/constants/astronomy-traits.js +12 -0
- package/dist/constants/astronomy-traits.js.map +1 -0
- package/dist/fits/FITSParser.d.ts +60 -0
- package/dist/fits/FITSParser.d.ts.map +1 -0
- package/dist/fits/FITSParser.js +234 -0
- package/dist/fits/FITSParser.js.map +1 -0
- package/dist/fits/FITSToGrid.d.ts +27 -0
- package/dist/fits/FITSToGrid.d.ts.map +1 -0
- package/dist/fits/FITSToGrid.js +85 -0
- package/dist/fits/FITSToGrid.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -7
- package/plugin.manifest.json +9 -0
- package/CHANGELOG.md +0 -19
- package/__tests__/SpectralCubeViewer.test.ts +0 -131
- package/__tests__/plugin.test.ts +0 -21
- package/python/astropy_bridge.py +0 -45
- package/src/bridge/python-runner.ts +0 -62
- package/src/components/SpectralCubeViewer.tsx +0 -335
- package/src/constants/astronomy-traits.ts +0 -13
- package/src/fits/FITSParser.ts +0 -312
- package/src/fits/FITSToGrid.ts +0 -96
- package/src/index.ts +0 -37
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -10
package/python/astropy_bridge.py
DELETED
|
@@ -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];
|