warplab 1.0.0

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.
@@ -0,0 +1,65 @@
1
+ // ─── Lightweight 3D vector for core computation ─────────────────────────
2
+ // Replaces THREE.Vector3 in server/CLI context. Implements only the
3
+ // operations used by geodesic integration and catalog processing.
4
+
5
+ export class Vec3 {
6
+ constructor(
7
+ public x = 0,
8
+ public y = 0,
9
+ public z = 0,
10
+ ) {}
11
+
12
+ clone(): Vec3 {
13
+ return new Vec3(this.x, this.y, this.z);
14
+ }
15
+
16
+ copy(v: Vec3): this {
17
+ this.x = v.x;
18
+ this.y = v.y;
19
+ this.z = v.z;
20
+ return this;
21
+ }
22
+
23
+ get length(): number {
24
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
25
+ }
26
+
27
+ get lengthSq(): number {
28
+ return this.x * this.x + this.y * this.y + this.z * this.z;
29
+ }
30
+
31
+ normalize(): this {
32
+ const l = this.length;
33
+ if (l > 0) {
34
+ this.x /= l;
35
+ this.y /= l;
36
+ this.z /= l;
37
+ }
38
+ return this;
39
+ }
40
+
41
+ multiplyScalar(s: number): this {
42
+ this.x *= s;
43
+ this.y *= s;
44
+ this.z *= s;
45
+ return this;
46
+ }
47
+
48
+ addScaledVector(v: Vec3, s: number): this {
49
+ this.x += v.x * s;
50
+ this.y += v.y * s;
51
+ this.z += v.z * s;
52
+ return this;
53
+ }
54
+
55
+ dot(v: Vec3): number {
56
+ return this.x * v.x + this.y * v.y + this.z * v.z;
57
+ }
58
+
59
+ crossVectors(a: Vec3, b: Vec3): this {
60
+ this.x = a.y * b.z - a.z * b.y;
61
+ this.y = a.z * b.x - a.x * b.z;
62
+ this.z = a.x * b.y - a.y * b.x;
63
+ return this;
64
+ }
65
+ }
@@ -0,0 +1,156 @@
1
+ // ─── Waveform Synthesis ─────────────────────────────────────────────────
2
+ // Simplified analytical IMRPhenom-like waveform generation.
3
+ // Pure computation — no browser dependencies.
4
+
5
+ import type { GWEvent, WaveformData, BinaryParams } from "./types";
6
+
7
+ /**
8
+ * Generate a synthetic IMRPhenom-like waveform for a catalog event.
9
+ *
10
+ * Simplified analytical approximation of inspiral-merger-ringdown.
11
+ * - Inspiral: frequency chirps as f(t) ~ (t_merger - t)^(-3/8)
12
+ * - Merger: peak amplitude
13
+ * - Ringdown: damped sinusoid at the remnant QNM frequency
14
+ */
15
+ export function generateWaveform(event: GWEvent): WaveformData {
16
+ const m1 = event.mass_1_source;
17
+ const m2 = event.mass_2_source;
18
+ const totalMass = m1 + m2;
19
+ const chirpMass = Math.pow(m1 * m2, 3 / 5) / Math.pow(totalMass, 1 / 5);
20
+
21
+ const sampleRate = 512;
22
+ const duration = Math.min(4.0, Math.max(1.5, 120 / chirpMass));
23
+ const numSamples = Math.floor(duration * sampleRate);
24
+ const mergerIndex = Math.floor(numSamples * 0.75);
25
+
26
+ const hPlus: number[] = new Array(numSamples);
27
+ const hCross: number[] = new Array(numSamples);
28
+
29
+ const fRingdown = 32000 / totalMass;
30
+ const tauRingdown = totalMass / 5000;
31
+
32
+ for (let i = 0; i < numSamples; i++) {
33
+ const t = i / sampleRate;
34
+ const tMerger = mergerIndex / sampleRate;
35
+
36
+ let amplitude: number;
37
+ let phase: number;
38
+
39
+ if (i < mergerIndex) {
40
+ const tau = Math.max(tMerger - t, 0.001);
41
+ const freqFactor = chirpMass / 30;
42
+ amplitude = 0.3 * Math.pow(0.5 / tau, 1 / 4);
43
+ phase =
44
+ -2 * Math.PI * 20 * Math.pow(0.5, 3 / 8) * (8 / 5) *
45
+ Math.pow(tau, 5 / 8) * freqFactor;
46
+ amplitude = Math.min(amplitude, 1.0);
47
+ } else {
48
+ const tPost = t - tMerger;
49
+ amplitude = Math.exp(-tPost / tauRingdown);
50
+ phase = 2 * Math.PI * fRingdown * tPost;
51
+ }
52
+
53
+ hPlus[i] = amplitude * Math.cos(phase);
54
+ hCross[i] = amplitude * Math.sin(phase);
55
+ }
56
+
57
+ // Normalize
58
+ let maxAmp = 0;
59
+ for (let i = 0; i < numSamples; i++) {
60
+ const a = Math.sqrt(hPlus[i] ** 2 + hCross[i] ** 2);
61
+ if (a > maxAmp) maxAmp = a;
62
+ }
63
+ if (maxAmp > 0) {
64
+ for (let i = 0; i < numSamples; i++) {
65
+ hPlus[i] /= maxAmp;
66
+ hCross[i] /= maxAmp;
67
+ }
68
+ }
69
+
70
+ return {
71
+ eventName: event.commonName,
72
+ sampleRate,
73
+ hPlus,
74
+ hCross,
75
+ duration,
76
+ peakIndex: mergerIndex,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Generate a synthetic IMRPhenom-like waveform from custom parameters.
82
+ * Includes spin-dependent ringdown and inclination effects.
83
+ */
84
+ export function generateCustomWaveform(params: BinaryParams): WaveformData {
85
+ const { m1, m2, chi1, chi2, inclination } = params;
86
+ const totalMass = m1 + m2;
87
+ const chirpMass = Math.pow(m1 * m2, 3 / 5) / Math.pow(totalMass, 1 / 5);
88
+
89
+ const chiEff = (m1 * chi1 + m2 * chi2) / totalMass;
90
+
91
+ const sampleRate = 512;
92
+ const duration = Math.min(4.0, Math.max(1.5, 120 / chirpMass));
93
+ const numSamples = Math.floor(duration * sampleRate);
94
+ const mergerIndex = Math.floor(numSamples * 0.75);
95
+
96
+ const hPlus: number[] = new Array(numSamples);
97
+ const hCross: number[] = new Array(numSamples);
98
+
99
+ // Ringdown frequency — spin correction
100
+ const fRingdown = (32000 / totalMass) * (1 + 0.15 * Math.abs(chiEff));
101
+ const tauRingdown = totalMass / 5000;
102
+
103
+ // Inclination affects relative amplitude of h+ vs h×
104
+ const cosInc = Math.cos(inclination);
105
+ const ampPlus = (1 + cosInc * cosInc) / 2;
106
+ const ampCross = cosInc;
107
+
108
+ for (let i = 0; i < numSamples; i++) {
109
+ const t = i / sampleRate;
110
+ const tMerger = mergerIndex / sampleRate;
111
+
112
+ let amplitude: number;
113
+ let phase: number;
114
+
115
+ if (i < mergerIndex) {
116
+ const tau = Math.max(tMerger - t, 0.001);
117
+ const freqFactor = chirpMass / 30;
118
+ amplitude = 0.3 * Math.pow(0.5 / tau, 1 / 4);
119
+ phase =
120
+ -2 * Math.PI * 20 * Math.pow(0.5, 3 / 8) * (8 / 5) *
121
+ Math.pow(tau, 5 / 8) * freqFactor;
122
+ amplitude = Math.min(amplitude, 1.0);
123
+ } else {
124
+ const tPost = t - tMerger;
125
+ amplitude = Math.exp(-tPost / tauRingdown);
126
+ phase = 2 * Math.PI * fRingdown * tPost;
127
+ }
128
+
129
+ hPlus[i] = amplitude * ampPlus * Math.cos(phase);
130
+ hCross[i] = amplitude * ampCross * Math.sin(phase);
131
+ }
132
+
133
+ // Normalize
134
+ let maxAmp = 0;
135
+ for (let i = 0; i < numSamples; i++) {
136
+ const a = Math.sqrt(hPlus[i] ** 2 + hCross[i] ** 2);
137
+ if (a > maxAmp) maxAmp = a;
138
+ }
139
+ if (maxAmp > 0) {
140
+ for (let i = 0; i < numSamples; i++) {
141
+ hPlus[i] /= maxAmp;
142
+ hCross[i] /= maxAmp;
143
+ }
144
+ }
145
+
146
+ const name = `Custom (${m1.toFixed(0)}+${m2.toFixed(0)} M\u2609)`;
147
+
148
+ return {
149
+ eventName: name,
150
+ sampleRate,
151
+ hPlus,
152
+ hCross,
153
+ duration,
154
+ peakIndex: mergerIndex,
155
+ };
156
+ }