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.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist-server/cli.js +1276 -0
- package/dist-server/mcp.js +1600 -0
- package/package.json +49 -0
- package/src/core/catalog.ts +134 -0
- package/src/core/export.ts +454 -0
- package/src/core/geodesic.ts +223 -0
- package/src/core/index.ts +53 -0
- package/src/core/multi-messenger.ts +77 -0
- package/src/core/noise-curve.ts +162 -0
- package/src/core/qnm.ts +97 -0
- package/src/core/types.ts +86 -0
- package/src/core/vec3.ts +65 -0
- package/src/core/waveform.ts +156 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "warplab",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Gravitational wave data tools — CLI and MCP server for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"warplab": "./dist-server/cli.js",
|
|
8
|
+
"warplab-mcp": "./dist-server/mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist-server/",
|
|
12
|
+
"src/core/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "tsc --noEmit && vite build",
|
|
17
|
+
"build:server": "tsup",
|
|
18
|
+
"prepublishOnly": "tsup",
|
|
19
|
+
"preview": "vite preview",
|
|
20
|
+
"download-strain": "python3 scripts/download-strain.py"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
25
|
+
"@vercel/analytics": "^1.6.1",
|
|
26
|
+
"fft.js": "^4.0.4",
|
|
27
|
+
"jszip": "^3.10.1",
|
|
28
|
+
"katex": "^0.16.33",
|
|
29
|
+
"motion": "^12.34.3",
|
|
30
|
+
"postprocessing": "^6.38.3",
|
|
31
|
+
"react": "^19.2.4",
|
|
32
|
+
"react-dom": "^19.2.4",
|
|
33
|
+
"tailwindcss": "^4.2.1",
|
|
34
|
+
"three": "^0.183.1",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"vite": "^7.3.1",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.2.14",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"@types/three": "^0.183.1",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vite-plugin-pwa": "^1.2.0"
|
|
45
|
+
},
|
|
46
|
+
"overrides": {
|
|
47
|
+
"serialize-javascript": "^7.0.4"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// ─── GWOSC Event Catalog ────────────────────────────────────────────────
|
|
2
|
+
// Fetch and process the gravitational wave event catalog from GWOSC.
|
|
3
|
+
// Pure computation — no browser dependencies.
|
|
4
|
+
|
|
5
|
+
import type { GWEvent } from "./types";
|
|
6
|
+
|
|
7
|
+
const GWOSC_API = "https://gwosc.org/eventapi/json/allevents/";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Simple seeded PRNG so event positions are stable across sessions.
|
|
11
|
+
* Uses the GPS timestamp as seed.
|
|
12
|
+
*/
|
|
13
|
+
function seededRandom(seed: number): () => number {
|
|
14
|
+
let s = seed;
|
|
15
|
+
return () => {
|
|
16
|
+
s = (s * 16807 + 0) % 2147483647;
|
|
17
|
+
return s / 2147483647;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch the full GWTC event catalog from GWOSC.
|
|
23
|
+
* The API returns a flat structure (parameters directly on each event object).
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchEventCatalog(): Promise<GWEvent[]> {
|
|
26
|
+
const res = await fetch(GWOSC_API);
|
|
27
|
+
if (!res.ok) throw new Error(`GWOSC API returned ${res.status}`);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
|
|
30
|
+
// Catalog priority: later catalogs have better parameter estimates
|
|
31
|
+
const catalogPriority: Record<string, number> = {
|
|
32
|
+
"O1_O2-Preliminary": 1,
|
|
33
|
+
"Initial_LIGO_Virgo": 1,
|
|
34
|
+
"GWTC-1-marginal": 2,
|
|
35
|
+
"GWTC-1-confident": 3,
|
|
36
|
+
"GWTC-2": 4,
|
|
37
|
+
"GWTC-2.1-marginal": 5,
|
|
38
|
+
"GWTC-2.1-auxiliary": 5,
|
|
39
|
+
"GWTC-2.1-confident": 6,
|
|
40
|
+
"GWTC-3-marginal": 7,
|
|
41
|
+
"GWTC-3-confident": 8,
|
|
42
|
+
"O3_Discovery_Papers": 8,
|
|
43
|
+
"O3_IMBH_marginal": 7,
|
|
44
|
+
"IAS-O3a": 5,
|
|
45
|
+
"GWTC-4.0": 9,
|
|
46
|
+
"O4_Discovery_Papers": 9,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const deduped = new Map<string, Record<string, unknown>>();
|
|
50
|
+
|
|
51
|
+
for (const [, entry] of Object.entries(data.events)) {
|
|
52
|
+
const e = entry as Record<string, unknown>;
|
|
53
|
+
const name = (e.commonName as string) ?? "";
|
|
54
|
+
if (!name) continue;
|
|
55
|
+
|
|
56
|
+
const existing = deduped.get(name);
|
|
57
|
+
if (existing) {
|
|
58
|
+
const existingPri = catalogPriority[(existing["catalog.shortName"] as string) ?? ""] ?? 0;
|
|
59
|
+
const newPri = catalogPriority[(e["catalog.shortName"] as string) ?? ""] ?? 0;
|
|
60
|
+
if (newPri > existingPri || (newPri === existingPri && !existing.mass_1_source && e.mass_1_source)) {
|
|
61
|
+
deduped.set(name, e);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
deduped.set(name, e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const events: GWEvent[] = [];
|
|
69
|
+
|
|
70
|
+
for (const [, e] of deduped) {
|
|
71
|
+
if (!e.mass_1_source || !e.mass_2_source) continue;
|
|
72
|
+
|
|
73
|
+
const gps = (e.GPS as number) ?? 0;
|
|
74
|
+
const distance = (e.luminosity_distance as number) ?? 0;
|
|
75
|
+
|
|
76
|
+
// Generate a deterministic sky position from the GPS timestamp
|
|
77
|
+
const rng = seededRandom(Math.floor(gps));
|
|
78
|
+
const ra = rng() * 2 * Math.PI;
|
|
79
|
+
const dec = Math.asin(2 * rng() - 1);
|
|
80
|
+
|
|
81
|
+
// Convert (distance, ra, dec) to cartesian (Mpc)
|
|
82
|
+
const r = distance;
|
|
83
|
+
const x = r * Math.cos(dec) * Math.cos(ra);
|
|
84
|
+
const y = r * Math.cos(dec) * Math.sin(ra);
|
|
85
|
+
const z = r * Math.sin(dec);
|
|
86
|
+
|
|
87
|
+
const event: GWEvent = {
|
|
88
|
+
commonName: (e.commonName as string) ?? "",
|
|
89
|
+
GPS: gps,
|
|
90
|
+
mass_1_source: e.mass_1_source as number,
|
|
91
|
+
mass_1_source_lower: (e.mass_1_source_lower as number) ?? 0,
|
|
92
|
+
mass_1_source_upper: (e.mass_1_source_upper as number) ?? 0,
|
|
93
|
+
mass_2_source: e.mass_2_source as number,
|
|
94
|
+
mass_2_source_lower: (e.mass_2_source_lower as number) ?? 0,
|
|
95
|
+
mass_2_source_upper: (e.mass_2_source_upper as number) ?? 0,
|
|
96
|
+
luminosity_distance: distance,
|
|
97
|
+
luminosity_distance_lower: (e.luminosity_distance_lower as number) ?? 0,
|
|
98
|
+
luminosity_distance_upper: (e.luminosity_distance_upper as number) ?? 0,
|
|
99
|
+
redshift: (e.redshift as number) ?? 0,
|
|
100
|
+
chi_eff: (e.chi_eff as number) ?? 0,
|
|
101
|
+
network_matched_filter_snr: (e.network_matched_filter_snr as number) ?? 0,
|
|
102
|
+
far: (e.far as number) ?? 0,
|
|
103
|
+
catalog_shortName: (e["catalog.shortName"] as string) ?? "",
|
|
104
|
+
total_mass_source: (e.total_mass_source as number) ?? 0,
|
|
105
|
+
chirp_mass_source: (e.chirp_mass_source as number) ?? 0,
|
|
106
|
+
chirp_mass_source_lower: (e.chirp_mass_source_lower as number) ?? 0,
|
|
107
|
+
chirp_mass_source_upper: (e.chirp_mass_source_upper as number) ?? 0,
|
|
108
|
+
final_mass_source: (e.final_mass_source as number) ?? 0,
|
|
109
|
+
final_mass_source_lower: (e.final_mass_source_lower as number) ?? 0,
|
|
110
|
+
final_mass_source_upper: (e.final_mass_source_upper as number) ?? 0,
|
|
111
|
+
p_astro: (e.p_astro as number) ?? 0,
|
|
112
|
+
mapPosition: { x, y, z },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
events.push(event);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Sort by SNR descending
|
|
119
|
+
events.sort(
|
|
120
|
+
(a, b) => b.network_matched_filter_snr - a.network_matched_filter_snr,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return events;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Classify an event by its component masses.
|
|
128
|
+
*/
|
|
129
|
+
export function classifyEvent(event: GWEvent): string {
|
|
130
|
+
const total = event.mass_1_source + event.mass_2_source;
|
|
131
|
+
if (total < 5) return "BNS";
|
|
132
|
+
if (event.mass_2_source < 3) return "NSBH";
|
|
133
|
+
return "BBH";
|
|
134
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// ─── Data Export Generation ─────────────────────────────────────────────
|
|
2
|
+
// Pure functions that generate export data in various formats.
|
|
3
|
+
// No DOM/download logic — that stays in the browser layer.
|
|
4
|
+
|
|
5
|
+
import type { GWEvent, WaveformData } from "./types";
|
|
6
|
+
import { classifyEvent } from "./catalog";
|
|
7
|
+
|
|
8
|
+
// ─── GWTC catalog → paper mapping ──────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const CATALOG_PAPERS: Record<string, { key: string; entry: string }> = {
|
|
11
|
+
"GWTC-1": {
|
|
12
|
+
key: "LIGOScientific:2018mvr",
|
|
13
|
+
entry: `@article{LIGOScientific:2018mvr,
|
|
14
|
+
author = "{LIGO Scientific Collaboration and Virgo Collaboration}",
|
|
15
|
+
title = "{GWTC-1: A Gravitational-Wave Transient Catalog of Compact Binary Mergers Observed by LIGO and Virgo during the First and Second Observing Runs}",
|
|
16
|
+
journal = "Phys. Rev. X",
|
|
17
|
+
volume = "9",
|
|
18
|
+
pages = "031040",
|
|
19
|
+
year = "2019",
|
|
20
|
+
doi = "10.1103/PhysRevX.9.031040",
|
|
21
|
+
eprint = "1811.12907",
|
|
22
|
+
archivePrefix = "arXiv"
|
|
23
|
+
}`,
|
|
24
|
+
},
|
|
25
|
+
"GWTC-2": {
|
|
26
|
+
key: "LIGOScientific:2020ibl",
|
|
27
|
+
entry: `@article{LIGOScientific:2020ibl,
|
|
28
|
+
author = "{LIGO Scientific Collaboration and Virgo Collaboration}",
|
|
29
|
+
title = "{GWTC-2: Compact Binary Coalescences Observed by LIGO and Virgo During the First Half of the Third Observing Run}",
|
|
30
|
+
journal = "Phys. Rev. X",
|
|
31
|
+
volume = "11",
|
|
32
|
+
pages = "021053",
|
|
33
|
+
year = "2021",
|
|
34
|
+
doi = "10.1103/PhysRevX.11.021053",
|
|
35
|
+
eprint = "2010.14527",
|
|
36
|
+
archivePrefix = "arXiv"
|
|
37
|
+
}`,
|
|
38
|
+
},
|
|
39
|
+
"GWTC-2.1": {
|
|
40
|
+
key: "LIGOScientific:2021usb",
|
|
41
|
+
entry: `@article{LIGOScientific:2021usb,
|
|
42
|
+
author = "{LIGO Scientific Collaboration and Virgo Collaboration}",
|
|
43
|
+
title = "{GWTC-2.1: Deep Extended Catalog of Compact Binary Coalescences Observed by LIGO and Virgo During the First Half of the Third Observing Run}",
|
|
44
|
+
journal = "Phys. Rev. D",
|
|
45
|
+
volume = "109",
|
|
46
|
+
pages = "022001",
|
|
47
|
+
year = "2024",
|
|
48
|
+
doi = "10.1103/PhysRevD.109.022001",
|
|
49
|
+
eprint = "2108.01045",
|
|
50
|
+
archivePrefix = "arXiv"
|
|
51
|
+
}`,
|
|
52
|
+
},
|
|
53
|
+
"GWTC-3": {
|
|
54
|
+
key: "LIGOScientific:2021djp",
|
|
55
|
+
entry: `@article{LIGOScientific:2021djp,
|
|
56
|
+
author = "{LIGO Scientific Collaboration and Virgo Collaboration and KAGRA Collaboration}",
|
|
57
|
+
title = "{GWTC-3: Compact Binary Coalescences Observed by LIGO and Virgo During the Second Part of the Third Observing Run}",
|
|
58
|
+
journal = "Phys. Rev. X",
|
|
59
|
+
volume = "13",
|
|
60
|
+
pages = "041039",
|
|
61
|
+
year = "2023",
|
|
62
|
+
doi = "10.1103/PhysRevX.13.041039",
|
|
63
|
+
eprint = "2111.03606",
|
|
64
|
+
archivePrefix = "arXiv"
|
|
65
|
+
}`,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ─── Parameter JSON ────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function generateParametersJSON(event: GWEvent): string {
|
|
72
|
+
const type = classifyEvent(event);
|
|
73
|
+
const energyRadiated = event.total_mass_source - event.final_mass_source;
|
|
74
|
+
|
|
75
|
+
const params = {
|
|
76
|
+
event: event.commonName,
|
|
77
|
+
catalog: event.catalog_shortName,
|
|
78
|
+
type,
|
|
79
|
+
gps_time: event.GPS,
|
|
80
|
+
mass_1_source: {
|
|
81
|
+
value: event.mass_1_source,
|
|
82
|
+
lower: event.mass_1_source_lower,
|
|
83
|
+
upper: event.mass_1_source_upper,
|
|
84
|
+
unit: "M_sun",
|
|
85
|
+
},
|
|
86
|
+
mass_2_source: {
|
|
87
|
+
value: event.mass_2_source,
|
|
88
|
+
lower: event.mass_2_source_lower,
|
|
89
|
+
upper: event.mass_2_source_upper,
|
|
90
|
+
unit: "M_sun",
|
|
91
|
+
},
|
|
92
|
+
total_mass_source: { value: event.total_mass_source, unit: "M_sun" },
|
|
93
|
+
chirp_mass_source: {
|
|
94
|
+
value: event.chirp_mass_source,
|
|
95
|
+
lower: event.chirp_mass_source_lower,
|
|
96
|
+
upper: event.chirp_mass_source_upper,
|
|
97
|
+
unit: "M_sun",
|
|
98
|
+
},
|
|
99
|
+
final_mass_source: {
|
|
100
|
+
value: event.final_mass_source,
|
|
101
|
+
lower: event.final_mass_source_lower,
|
|
102
|
+
upper: event.final_mass_source_upper,
|
|
103
|
+
unit: "M_sun",
|
|
104
|
+
},
|
|
105
|
+
energy_radiated: { value: energyRadiated, unit: "M_sun_c2" },
|
|
106
|
+
luminosity_distance: {
|
|
107
|
+
value: event.luminosity_distance,
|
|
108
|
+
lower: event.luminosity_distance_lower,
|
|
109
|
+
upper: event.luminosity_distance_upper,
|
|
110
|
+
unit: "Mpc",
|
|
111
|
+
},
|
|
112
|
+
redshift: event.redshift,
|
|
113
|
+
chi_eff: event.chi_eff,
|
|
114
|
+
network_snr: event.network_matched_filter_snr,
|
|
115
|
+
false_alarm_rate: event.far,
|
|
116
|
+
p_astro: event.p_astro,
|
|
117
|
+
source: "GWOSC (https://gwosc.org)",
|
|
118
|
+
exported_by: "WarpLab (https://warplab.app)",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return JSON.stringify(params, null, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Parameter CSV ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export function generateParametersCSV(event: GWEvent): string {
|
|
127
|
+
const type = classifyEvent(event);
|
|
128
|
+
const energyRadiated = event.total_mass_source - event.final_mass_source;
|
|
129
|
+
|
|
130
|
+
const headers = ["parameter", "value", "lower_90", "upper_90", "unit"];
|
|
131
|
+
|
|
132
|
+
const rows = [
|
|
133
|
+
["event_name", event.commonName, "", "", ""],
|
|
134
|
+
["catalog", event.catalog_shortName, "", "", ""],
|
|
135
|
+
["type", type, "", "", ""],
|
|
136
|
+
["gps_time", event.GPS, "", "", "s"],
|
|
137
|
+
["mass_1_source", event.mass_1_source, event.mass_1_source_lower, event.mass_1_source_upper, "M_sun"],
|
|
138
|
+
["mass_2_source", event.mass_2_source, event.mass_2_source_lower, event.mass_2_source_upper, "M_sun"],
|
|
139
|
+
["total_mass_source", event.total_mass_source, "", "", "M_sun"],
|
|
140
|
+
["chirp_mass_source", event.chirp_mass_source, event.chirp_mass_source_lower, event.chirp_mass_source_upper, "M_sun"],
|
|
141
|
+
["final_mass_source", event.final_mass_source, event.final_mass_source_lower, event.final_mass_source_upper, "M_sun"],
|
|
142
|
+
["energy_radiated", energyRadiated, "", "", "M_sun_c2"],
|
|
143
|
+
["luminosity_distance", event.luminosity_distance, event.luminosity_distance_lower, event.luminosity_distance_upper, "Mpc"],
|
|
144
|
+
["redshift", event.redshift, "", "", ""],
|
|
145
|
+
["chi_eff", event.chi_eff, "", "", ""],
|
|
146
|
+
["network_snr", event.network_matched_filter_snr, "", "", ""],
|
|
147
|
+
["false_alarm_rate", event.far, "", "", "Hz"],
|
|
148
|
+
["p_astro", event.p_astro, "", "", ""],
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Waveform CSV ──────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export function generateWaveformCSV(waveform: WaveformData): string {
|
|
157
|
+
const headers = ["time_s", "h_plus", "h_cross"];
|
|
158
|
+
const lines = [headers.join(",")];
|
|
159
|
+
const dt = 1 / waveform.sampleRate;
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < waveform.hPlus.length; i++) {
|
|
162
|
+
const t = (i * dt).toFixed(6);
|
|
163
|
+
lines.push(`${t},${waveform.hPlus[i].toExponential(8)},${waveform.hCross[i].toExponential(8)}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── BibTeX ────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function generateBibTeX(event: GWEvent): string {
|
|
172
|
+
const entries: string[] = [];
|
|
173
|
+
|
|
174
|
+
entries.push(`@misc{GWOSC,
|
|
175
|
+
author = "{LIGO Scientific Collaboration and Virgo Collaboration and KAGRA Collaboration}",
|
|
176
|
+
title = "{Gravitational Wave Open Science Center}",
|
|
177
|
+
howpublished = "\\url{https://gwosc.org}",
|
|
178
|
+
year = "2023",
|
|
179
|
+
note = "Event: ${event.commonName}"
|
|
180
|
+
}`);
|
|
181
|
+
|
|
182
|
+
const catalog = event.catalog_shortName;
|
|
183
|
+
const paper = CATALOG_PAPERS[catalog];
|
|
184
|
+
if (paper) {
|
|
185
|
+
entries.push(paper.entry);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
entries.push(`@misc{WarpLab,
|
|
189
|
+
author = "{Canton, Daniel}",
|
|
190
|
+
title = "{WarpLab: Interactive Gravitational Wave Visualizer}",
|
|
191
|
+
howpublished = "\\url{https://warplab.app}",
|
|
192
|
+
year = "2025"
|
|
193
|
+
}`);
|
|
194
|
+
|
|
195
|
+
return entries.join("\n\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Jupyter Notebook ──────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
export function generateNotebook(event: GWEvent): string {
|
|
201
|
+
const type = classifyEvent(event);
|
|
202
|
+
const gps = event.GPS;
|
|
203
|
+
const eventName = event.commonName;
|
|
204
|
+
|
|
205
|
+
const notebook = {
|
|
206
|
+
nbformat: 4,
|
|
207
|
+
nbformat_minor: 5,
|
|
208
|
+
metadata: {
|
|
209
|
+
kernelspec: {
|
|
210
|
+
display_name: "Python 3",
|
|
211
|
+
language: "python",
|
|
212
|
+
name: "python3",
|
|
213
|
+
},
|
|
214
|
+
language_info: {
|
|
215
|
+
name: "python",
|
|
216
|
+
version: "3.10.0",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
cells: [
|
|
220
|
+
{
|
|
221
|
+
cell_type: "markdown",
|
|
222
|
+
metadata: {},
|
|
223
|
+
source: [
|
|
224
|
+
`# ${eventName} \u2014 Gravitational Wave Analysis\n`,
|
|
225
|
+
`\n`,
|
|
226
|
+
`**Type:** ${type} \n`,
|
|
227
|
+
`**Masses:** ${event.mass_1_source.toFixed(1)} + ${event.mass_2_source.toFixed(1)} M\u2609 \n`,
|
|
228
|
+
`**Distance:** ${event.luminosity_distance.toFixed(0)} Mpc \n`,
|
|
229
|
+
`**Catalog:** ${event.catalog_shortName} \n`,
|
|
230
|
+
`**GPS Time:** ${gps} \n`,
|
|
231
|
+
`\n`,
|
|
232
|
+
`This notebook fetches real detector strain from [GWOSC](https://gwosc.org) and reproduces the spectrogram and template overlay.\n`,
|
|
233
|
+
`\n`,
|
|
234
|
+
`*Exported from [WarpLab](https://warplab.app)*`,
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
cell_type: "markdown",
|
|
239
|
+
metadata: {},
|
|
240
|
+
source: ["## 1. Setup\n", "Install required packages if needed."],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
cell_type: "code",
|
|
244
|
+
metadata: {},
|
|
245
|
+
source: [
|
|
246
|
+
`# Install dependencies (uncomment if needed)\n`,
|
|
247
|
+
`# !pip install gwosc gwpy matplotlib numpy\n`,
|
|
248
|
+
`\n`,
|
|
249
|
+
`import numpy as np\n`,
|
|
250
|
+
`import matplotlib.pyplot as plt\n`,
|
|
251
|
+
`from gwpy.timeseries import TimeSeries\n`,
|
|
252
|
+
`from gwosc.datasets import event_gps\n`,
|
|
253
|
+
`\n`,
|
|
254
|
+
`EVENT = "${eventName}"\n`,
|
|
255
|
+
`GPS = ${gps}\n`,
|
|
256
|
+
`DETECTOR = "H1" # Change to "L1" or "V1" for other detectors`,
|
|
257
|
+
],
|
|
258
|
+
execution_count: null,
|
|
259
|
+
outputs: [],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
cell_type: "markdown",
|
|
263
|
+
metadata: {},
|
|
264
|
+
source: [
|
|
265
|
+
"## 2. Fetch strain data from GWOSC\n",
|
|
266
|
+
"Download 32 seconds of strain centered on the event.",
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
cell_type: "code",
|
|
271
|
+
metadata: {},
|
|
272
|
+
source: [
|
|
273
|
+
`# Fetch 32s of strain data centered on the event\n`,
|
|
274
|
+
`strain = TimeSeries.fetch_open_data(\n`,
|
|
275
|
+
` DETECTOR, GPS - 16, GPS + 16,\n`,
|
|
276
|
+
` cache=True\n`,
|
|
277
|
+
`)\n`,
|
|
278
|
+
`print(f"Sample rate: {strain.sample_rate}")\n`,
|
|
279
|
+
`print(f"Duration: {strain.duration}")`,
|
|
280
|
+
],
|
|
281
|
+
execution_count: null,
|
|
282
|
+
outputs: [],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
cell_type: "markdown",
|
|
286
|
+
metadata: {},
|
|
287
|
+
source: [
|
|
288
|
+
"## 3. Q-transform spectrogram\n",
|
|
289
|
+
"Compute and plot the time-frequency spectrogram using a Q-transform.",
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
cell_type: "code",
|
|
294
|
+
metadata: {},
|
|
295
|
+
source: [
|
|
296
|
+
`# Compute Q-transform spectrogram\n`,
|
|
297
|
+
`dt = 1 # seconds around merger to plot\n`,
|
|
298
|
+
`qgram = strain.q_transform(\n`,
|
|
299
|
+
` outseg=(GPS - dt, GPS + dt),\n`,
|
|
300
|
+
` qrange=(4, 64),\n`,
|
|
301
|
+
` frange=(20, 1024),\n`,
|
|
302
|
+
` logf=True\n`,
|
|
303
|
+
`)\n`,
|
|
304
|
+
`\n`,
|
|
305
|
+
`fig, ax = plt.subplots(figsize=(10, 5))\n`,
|
|
306
|
+
`ax.imshow(qgram.T, origin="lower", aspect="auto",\n`,
|
|
307
|
+
` extent=[qgram.x0.value, (qgram.x0 + qgram.dx * qgram.shape[0]).value,\n`,
|
|
308
|
+
` qgram.y0.value, (qgram.y0 + qgram.dy * qgram.shape[1]).value])\n`,
|
|
309
|
+
`ax.set_xlabel("Time [s]")\n`,
|
|
310
|
+
`ax.set_ylabel("Frequency [Hz]")\n`,
|
|
311
|
+
`ax.set_title(f"{EVENT} \u2014 Q-transform ({DETECTOR})")\n`,
|
|
312
|
+
`ax.set_yscale("log")\n`,
|
|
313
|
+
`plt.colorbar(ax.images[0], label="Normalized energy")\n`,
|
|
314
|
+
`plt.tight_layout()\n`,
|
|
315
|
+
`plt.show()`,
|
|
316
|
+
],
|
|
317
|
+
execution_count: null,
|
|
318
|
+
outputs: [],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
cell_type: "markdown",
|
|
322
|
+
metadata: {},
|
|
323
|
+
source: [
|
|
324
|
+
"## 4. Whitened strain and template overlay\n",
|
|
325
|
+
"Bandpass and whiten the data, then overlay the included waveform template.",
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
cell_type: "code",
|
|
330
|
+
metadata: {},
|
|
331
|
+
source: [
|
|
332
|
+
`# Bandpass filter and whiten\n`,
|
|
333
|
+
`white = strain.whiten(4, 2).bandpass(30, 400)\n`,
|
|
334
|
+
`\n`,
|
|
335
|
+
`# Load the template waveform from the exported CSV\n`,
|
|
336
|
+
`import os\n`,
|
|
337
|
+
`template_file = os.path.join(\n`,
|
|
338
|
+
` os.path.dirname(os.path.abspath("__file__")),\n`,
|
|
339
|
+
` "waveform_template.csv"\n`,
|
|
340
|
+
`)\n`,
|
|
341
|
+
`\n`,
|
|
342
|
+
`fig, ax = plt.subplots(figsize=(10, 4))\n`,
|
|
343
|
+
`\n`,
|
|
344
|
+
`# Plot whitened strain around merger\n`,
|
|
345
|
+
`t = white.times.value - GPS\n`,
|
|
346
|
+
`mask = (t > -0.5) & (t < 0.2)\n`,
|
|
347
|
+
`ax.plot(t[mask], white.value[mask], label=f"{DETECTOR} whitened\", alpha=0.8)\n`,
|
|
348
|
+
`\n`,
|
|
349
|
+
`# Overlay template if available\n`,
|
|
350
|
+
`if os.path.exists(template_file):\n`,
|
|
351
|
+
` template = np.genfromtxt(template_file, delimiter=",",\n`,
|
|
352
|
+
` names=True, dtype=None, encoding="utf-8")\n`,
|
|
353
|
+
` t_templ = template["time_s"]\n`,
|
|
354
|
+
` h_templ = template["h_plus"]\n`,
|
|
355
|
+
` # Center template on t=0 at peak\n`,
|
|
356
|
+
` peak_idx = np.argmax(np.abs(h_templ))\n`,
|
|
357
|
+
` t_templ = t_templ - t_templ[peak_idx]\n`,
|
|
358
|
+
` # Scale template to match whitened strain amplitude\n`,
|
|
359
|
+
` scale = np.max(np.abs(white.value[mask])) / np.max(np.abs(h_templ))\n`,
|
|
360
|
+
` ax.plot(t_templ, h_templ * scale, "--", label="Template (h+)",\n`,
|
|
361
|
+
` alpha=0.7, color="tab:orange")\n`,
|
|
362
|
+
`\n`,
|
|
363
|
+
`ax.set_xlabel("Time relative to merger [s]")\n`,
|
|
364
|
+
`ax.set_ylabel("Strain (whitened)")\n`,
|
|
365
|
+
`ax.set_title(f"{EVENT} \u2014 Whitened strain with template overlay")\n`,
|
|
366
|
+
`ax.legend()\n`,
|
|
367
|
+
`plt.tight_layout()\n`,
|
|
368
|
+
`plt.show()`,
|
|
369
|
+
],
|
|
370
|
+
execution_count: null,
|
|
371
|
+
outputs: [],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
cell_type: "markdown",
|
|
375
|
+
metadata: {},
|
|
376
|
+
source: [
|
|
377
|
+
"## 5. Event parameters\n",
|
|
378
|
+
"Summary of parameters from the GWTC catalog.",
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
cell_type: "code",
|
|
383
|
+
metadata: {},
|
|
384
|
+
source: [
|
|
385
|
+
`# Event parameters from ${event.catalog_shortName}\n`,
|
|
386
|
+
`params = {\n`,
|
|
387
|
+
` "Event": "${eventName}",\n`,
|
|
388
|
+
` "Type": "${type}",\n`,
|
|
389
|
+
` "m1 [M\u2609]": ${event.mass_1_source.toFixed(2)},\n`,
|
|
390
|
+
` "m2 [M\u2609]": ${event.mass_2_source.toFixed(2)},\n`,
|
|
391
|
+
` "M_total [M\u2609]": ${event.total_mass_source.toFixed(2)},\n`,
|
|
392
|
+
` "M_chirp [M\u2609]": ${event.chirp_mass_source.toFixed(2)},\n`,
|
|
393
|
+
` "M_final [M\u2609]": ${event.final_mass_source.toFixed(2)},\n`,
|
|
394
|
+
` "Distance [Mpc]": ${event.luminosity_distance.toFixed(1)},\n`,
|
|
395
|
+
` "Redshift": ${event.redshift.toFixed(4)},\n`,
|
|
396
|
+
` "\u03c7_eff": ${event.chi_eff.toFixed(3)},\n`,
|
|
397
|
+
` "SNR": ${event.network_matched_filter_snr.toFixed(1)},\n`,
|
|
398
|
+
` "p_astro": ${event.p_astro.toFixed(4)},\n`,
|
|
399
|
+
`}\n`,
|
|
400
|
+
`\n`,
|
|
401
|
+
`for k, v in params.items():\n`,
|
|
402
|
+
` print(f"{k:20s} {v}")`,
|
|
403
|
+
],
|
|
404
|
+
execution_count: null,
|
|
405
|
+
outputs: [],
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return JSON.stringify(notebook, null, 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── README ────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
export function generateREADME(event: GWEvent): string {
|
|
416
|
+
const type = classifyEvent(event);
|
|
417
|
+
return `# ${event.commonName} \u2014 Data Export
|
|
418
|
+
|
|
419
|
+
Type: ${type}
|
|
420
|
+
Masses: ${event.mass_1_source.toFixed(1)} + ${event.mass_2_source.toFixed(1)} M\u2609
|
|
421
|
+
Distance: ${event.luminosity_distance.toFixed(0)} Mpc
|
|
422
|
+
Catalog: ${event.catalog_shortName}
|
|
423
|
+
|
|
424
|
+
## Files
|
|
425
|
+
|
|
426
|
+
- **parameters.json** \u2014 Full event parameters with uncertainties (JSON)
|
|
427
|
+
- **parameters.csv** \u2014 Same parameters in tabular CSV format
|
|
428
|
+
- **waveform_template.csv** \u2014 Synthetic IMRPhenom waveform: h+(t) and h\u00d7(t) arrays
|
|
429
|
+
- **notebook.ipynb** \u2014 Jupyter notebook that fetches real strain from GWOSC and reproduces the analysis
|
|
430
|
+
- **CITATION.bib** \u2014 BibTeX citations for GWOSC, the catalog paper, and WarpLab
|
|
431
|
+
|
|
432
|
+
## Using the notebook
|
|
433
|
+
|
|
434
|
+
1. Install dependencies: \`pip install gwosc gwpy matplotlib numpy\`
|
|
435
|
+
2. Open \`notebook.ipynb\` in JupyterLab or VS Code
|
|
436
|
+
3. Run all cells \u2014 it will download real detector strain from GWOSC
|
|
437
|
+
4. The notebook produces a Q-transform spectrogram and a whitened strain plot with the template overlay
|
|
438
|
+
|
|
439
|
+
## Data source
|
|
440
|
+
|
|
441
|
+
All parameters are from the Gravitational Wave Open Science Center (GWOSC):
|
|
442
|
+
https://gwosc.org
|
|
443
|
+
|
|
444
|
+
The waveform template is a simplified IMRPhenom analytical approximation generated by WarpLab.
|
|
445
|
+
It is NOT a full numerical relativity waveform. For research use, fetch real strain from GWOSC.
|
|
446
|
+
|
|
447
|
+
## Citation
|
|
448
|
+
|
|
449
|
+
If you use this data in academic work, please cite the sources in CITATION.bib.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
Exported from WarpLab (https://warplab.app)
|
|
453
|
+
`;
|
|
454
|
+
}
|