sgn-drift 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/PKG-INFO +1 -1
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/pyproject.toml +2 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/PKG-INFO +1 -1
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/SOURCES.txt +2 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/entry_points.txt +2 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/_version.py +3 -3
- sgn_drift-0.1.1/src/sgndrift/bin/estimate_psd.py +240 -0
- sgn_drift-0.1.1/src/sgndrift/bin/plot_psd.py +280 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/.gitignore +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/.gitlab-ci.yml +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/README.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/build_requirements.txt +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/api/sinks/drift_sink.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/api/transforms/drift.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/api/transforms/psd.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/api/util/drift.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/api/util/psd_estimators.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/docs/index.md +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/mkdocs.yml +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/setup.cfg +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/dependency_links.txt +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/requires.txt +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgn_drift.egg-info/top_level.txt +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/__init__.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/__init__.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/estimate_drift.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/plot_drift.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/plot_drift_comparison.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/plot_drift_super.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/plot_drift_super_comp.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/bin/plot_drift_time.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/psd/__init__.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/psd/drift.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/psd/estimators.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/sinks/__init__.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/sinks/drift_sink.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/transforms/__init__.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/transforms/drift.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/src/sgndrift/transforms/psd.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/tests/test_psd_drift.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/tests/test_psd_estimators.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/tests/test_sinks_drift_sink.py +0 -0
- {sgn_drift-0.1.0 → sgn_drift-0.1.1}/tests/test_transforms_drift.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sgn-drift
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: PSD Drift Data Quality Extensions for the SGN Framework
|
|
5
5
|
Author-email: James Kennington <jmk7376@psu.edu>, Zach Yarbrough <jmk7376@psu.edu>, Joshua Black <jmk7376@psu.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -128,7 +128,9 @@ exclude = [
|
|
|
128
128
|
|
|
129
129
|
[project.scripts]
|
|
130
130
|
sgn-drift-estimate = "sgndrift.bin.estimate_drift:main"
|
|
131
|
+
sgn-drift-psd = "sgndrift.bin.estimate_psd:main"
|
|
131
132
|
sgn-drift-plot = "sgndrift.bin.plot_drift:main"
|
|
133
|
+
sgn-drift-plot-psd = "sgndrift.bin.plot_psd:main"
|
|
132
134
|
sgn-drift-plot-comp = "sgndrift.bin.plot_drift_comparison:main"
|
|
133
135
|
sgn-drift-plot-time = "sgndrift.bin.plot_drift_time:main"
|
|
134
136
|
sgn-drift-plot-super = "sgndrift.bin.plot_drift_super:main"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sgn-drift
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: PSD Drift Data Quality Extensions for the SGN Framework
|
|
5
5
|
Author-email: James Kennington <jmk7376@psu.edu>, Zach Yarbrough <jmk7376@psu.edu>, Joshua Black <jmk7376@psu.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -20,11 +20,13 @@ src/sgndrift/__init__.py
|
|
|
20
20
|
src/sgndrift/_version.py
|
|
21
21
|
src/sgndrift/bin/__init__.py
|
|
22
22
|
src/sgndrift/bin/estimate_drift.py
|
|
23
|
+
src/sgndrift/bin/estimate_psd.py
|
|
23
24
|
src/sgndrift/bin/plot_drift.py
|
|
24
25
|
src/sgndrift/bin/plot_drift_comparison.py
|
|
25
26
|
src/sgndrift/bin/plot_drift_super.py
|
|
26
27
|
src/sgndrift/bin/plot_drift_super_comp.py
|
|
27
28
|
src/sgndrift/bin/plot_drift_time.py
|
|
29
|
+
src/sgndrift/bin/plot_psd.py
|
|
28
30
|
src/sgndrift/psd/__init__.py
|
|
29
31
|
src/sgndrift/psd/drift.py
|
|
30
32
|
src/sgndrift/psd/estimators.py
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
sgn-drift-estimate = sgndrift.bin.estimate_drift:main
|
|
3
3
|
sgn-drift-plot = sgndrift.bin.plot_drift:main
|
|
4
4
|
sgn-drift-plot-comp = sgndrift.bin.plot_drift_comparison:main
|
|
5
|
+
sgn-drift-plot-psd = sgndrift.bin.plot_psd:main
|
|
5
6
|
sgn-drift-plot-super = sgndrift.bin.plot_drift_super:main
|
|
6
7
|
sgn-drift-plot-super-comp = sgndrift.bin.plot_drift_super_comp:main
|
|
7
8
|
sgn-drift-plot-time = sgndrift.bin.plot_drift_time:main
|
|
9
|
+
sgn-drift-psd = sgndrift.bin.estimate_psd:main
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g3258d2ad7'
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Robust PSD Estimation Tool.
|
|
4
|
+
|
|
5
|
+
This script estimates the Power Spectral Density (PSD) of gravitational wave data.
|
|
6
|
+
It generalizes legacy functionality by leveraging the SGN framework for:
|
|
7
|
+
1. Robust data fetching (GWOSC).
|
|
8
|
+
2. Modern PSD estimators (MGM, Recursive).
|
|
9
|
+
3. Flexible output formatting via PSDSink.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
# SGN & Family
|
|
19
|
+
from sgn.apps import Pipeline
|
|
20
|
+
from sgn.base import SourceElement
|
|
21
|
+
from sgndrift.transforms.psd import MGMPSD, RecursivePSD
|
|
22
|
+
from sgnligo.sinks.psd_sink import PSDSink
|
|
23
|
+
from sgnligo.sources.gwosc import GWOSCSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_pipeline(
|
|
27
|
+
start: float,
|
|
28
|
+
end: float,
|
|
29
|
+
detectors: List[str],
|
|
30
|
+
sample_rate: int,
|
|
31
|
+
fft_length: float,
|
|
32
|
+
overlap: float,
|
|
33
|
+
estimator_type: str,
|
|
34
|
+
output_fmt: str,
|
|
35
|
+
write_interval: Optional[float] = None,
|
|
36
|
+
source_element: Optional[SourceElement] = None,
|
|
37
|
+
verbose: bool = False,
|
|
38
|
+
) -> Pipeline:
|
|
39
|
+
"""Constructs the processing pipeline for the requested interval."""
|
|
40
|
+
pipe = Pipeline()
|
|
41
|
+
|
|
42
|
+
# 1. Source
|
|
43
|
+
if source_element:
|
|
44
|
+
source = source_element
|
|
45
|
+
source_name = source.name
|
|
46
|
+
else:
|
|
47
|
+
source_name = f"gwosc_{int(start)}"
|
|
48
|
+
source = GWOSCSource(
|
|
49
|
+
name=source_name,
|
|
50
|
+
start=start,
|
|
51
|
+
end=end,
|
|
52
|
+
detectors=detectors,
|
|
53
|
+
sample_rate=sample_rate,
|
|
54
|
+
cache_data=True,
|
|
55
|
+
verbose=verbose,
|
|
56
|
+
)
|
|
57
|
+
pipe.insert(source)
|
|
58
|
+
|
|
59
|
+
# 2. Per-Detector Processing
|
|
60
|
+
for ifo in detectors:
|
|
61
|
+
# A. Estimator
|
|
62
|
+
est_name = f"psd_{ifo}"
|
|
63
|
+
if estimator_type.lower() == "recursive":
|
|
64
|
+
est = RecursivePSD(
|
|
65
|
+
name=est_name,
|
|
66
|
+
sink_pad_names=("in",),
|
|
67
|
+
source_pad_names=("out",),
|
|
68
|
+
fft_length=fft_length,
|
|
69
|
+
overlap=overlap,
|
|
70
|
+
sample_rate=int(sample_rate),
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
# Default to MGM
|
|
74
|
+
est = MGMPSD(
|
|
75
|
+
name=est_name,
|
|
76
|
+
sink_pad_names=("in",),
|
|
77
|
+
source_pad_names=("out",),
|
|
78
|
+
fft_length=fft_length,
|
|
79
|
+
overlap=overlap,
|
|
80
|
+
sample_rate=int(sample_rate),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# B. Sink
|
|
84
|
+
# Format filename: defaults to "psd_{detector}_{start}.xml.gz"
|
|
85
|
+
fname = output_fmt.format(
|
|
86
|
+
detector=ifo,
|
|
87
|
+
start="{gps}",
|
|
88
|
+
duration=int(end - start),
|
|
89
|
+
rate=int(sample_rate),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# PSDSink usually expects the IFO name as the sink pad
|
|
93
|
+
# to correctly label the XML element
|
|
94
|
+
sink = PSDSink(
|
|
95
|
+
name=f"sink_{ifo}",
|
|
96
|
+
sink_pad_names=(ifo,),
|
|
97
|
+
fname=fname,
|
|
98
|
+
write_interval=write_interval,
|
|
99
|
+
verbose=verbose,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
pipe.insert(est, sink)
|
|
103
|
+
|
|
104
|
+
# C. Link
|
|
105
|
+
# Source[IFO] -> Est[in]
|
|
106
|
+
# Est[out] -> Sink[IFO]
|
|
107
|
+
|
|
108
|
+
# Check source pads
|
|
109
|
+
if ifo in source.srcs:
|
|
110
|
+
src_pad = source.srcs[ifo]
|
|
111
|
+
else:
|
|
112
|
+
if verbose:
|
|
113
|
+
print(
|
|
114
|
+
f"Warning: Source pad '{ifo}' not found in source '{source.name}'. Skipping {ifo}."
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
pipe.link({est.snks["in"]: src_pad, sink.snks[ifo]: est.srcs["out"]})
|
|
119
|
+
|
|
120
|
+
return pipe
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def estimate_psd(
|
|
124
|
+
start: float,
|
|
125
|
+
end: float,
|
|
126
|
+
detectors: List[str],
|
|
127
|
+
output_fmt: str = "psd_{detector}_{start}.xml.gz",
|
|
128
|
+
estimator: str = "mgm",
|
|
129
|
+
fft_length: float = 4.0,
|
|
130
|
+
overlap: float = 0.5,
|
|
131
|
+
sample_rate: float = 4096.0,
|
|
132
|
+
write_interval: Optional[float] = None,
|
|
133
|
+
verbose: bool = False,
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Main driver for PSD estimation.
|
|
137
|
+
Runs the pipeline over the user-specified [start, end) interval.
|
|
138
|
+
"""
|
|
139
|
+
duration = end - start
|
|
140
|
+
if duration < (fft_length * 4):
|
|
141
|
+
print(f"Error: Duration ({duration}s) is too short for stable PSD estimation.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if verbose:
|
|
145
|
+
print(f"Processing interval: {start} - {end} ({duration}s)")
|
|
146
|
+
print(f"Detectors: {detectors}")
|
|
147
|
+
print(f"Estimator: {estimator}")
|
|
148
|
+
if write_interval:
|
|
149
|
+
print(f"Write Interval: {write_interval}s")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
pipe = build_pipeline(
|
|
153
|
+
start=start,
|
|
154
|
+
end=end,
|
|
155
|
+
detectors=detectors,
|
|
156
|
+
sample_rate=int(sample_rate),
|
|
157
|
+
fft_length=fft_length,
|
|
158
|
+
overlap=overlap,
|
|
159
|
+
estimator_type=estimator,
|
|
160
|
+
output_fmt=output_fmt,
|
|
161
|
+
write_interval=write_interval,
|
|
162
|
+
verbose=verbose,
|
|
163
|
+
)
|
|
164
|
+
pipe.run()
|
|
165
|
+
if verbose:
|
|
166
|
+
print("Pipeline finished successfully.")
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"Pipeline failed: {e}")
|
|
170
|
+
import traceback
|
|
171
|
+
|
|
172
|
+
traceback.print_exc()
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_args():
|
|
177
|
+
parser = argparse.ArgumentParser(description="Estimate GW PSDs.")
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--detectors", type=str, required=True, help="Comma-separated list (e.g. H1,L1)"
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument("--start", type=float, required=True, help="GPS Start Time")
|
|
182
|
+
parser.add_argument("--end", type=float, required=True, help="GPS End Time")
|
|
183
|
+
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--output-fmt",
|
|
186
|
+
type=str,
|
|
187
|
+
default="psd_{detector}_{start}.xml.gz",
|
|
188
|
+
help="Output filename format. Available keys: {detector}, {start}, {duration}, {rate}",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--estimator",
|
|
193
|
+
type=str,
|
|
194
|
+
default="mgm",
|
|
195
|
+
choices=["mgm", "recursive"],
|
|
196
|
+
help="PSD Estimation algorithm",
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument(
|
|
199
|
+
"--fft-length", type=float, default=4.0, help="FFT length in seconds"
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"--overlap", type=float, default=0.5, help="Overlap fraction (0-1)"
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--sample-rate", type=float, default=4096.0, help="Sampling rate in Hz"
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"--write-interval",
|
|
209
|
+
type=float,
|
|
210
|
+
default=None,
|
|
211
|
+
help="Interval in seconds to write intermediate PSDs to disk.",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"-v", "--verbose", action="store_true", help="Enable verbose logging"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return parser.parse_args()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main():
|
|
222
|
+
args = parse_args()
|
|
223
|
+
dets = [d.strip() for d in args.detectors.split(",")]
|
|
224
|
+
|
|
225
|
+
estimate_psd(
|
|
226
|
+
start=args.start,
|
|
227
|
+
end=args.end,
|
|
228
|
+
detectors=dets,
|
|
229
|
+
output_fmt=args.output_fmt,
|
|
230
|
+
estimator=args.estimator,
|
|
231
|
+
fft_length=args.fft_length,
|
|
232
|
+
overlap=args.overlap,
|
|
233
|
+
sample_rate=args.sample_rate,
|
|
234
|
+
write_interval=args.write_interval,
|
|
235
|
+
verbose=args.verbose,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PSD Visualization Tool.
|
|
4
|
+
|
|
5
|
+
Generates comparison plots of Power Spectral Densities (PSDs).
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Supports multiple input files (LIGO LW XML, HDF5, TXT).
|
|
9
|
+
- Auto-detects Time Evolution (Drift) using internal metadata.
|
|
10
|
+
- Smart Labeling (Relative Time).
|
|
11
|
+
- Sequential Colormaps for Drift Plots.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from typing import List, Optional, Any
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import matplotlib.cm as cm
|
|
22
|
+
import numpy as np
|
|
23
|
+
from gwpy.frequencyseries import FrequencySeries
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_psd(filepath: str, ifo: Optional[str] = None) -> FrequencySeries:
|
|
27
|
+
"""Loads a PSD FrequencySeries from a file using GWpy."""
|
|
28
|
+
if not os.path.exists(filepath):
|
|
29
|
+
print(f"Error: File not found at {filepath}")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# For XML files, explicit IFO helps GWpy disambiguate
|
|
34
|
+
if ifo:
|
|
35
|
+
return FrequencySeries.read(filepath, instrument=ifo)
|
|
36
|
+
else:
|
|
37
|
+
return FrequencySeries.read(filepath)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Error reading {filepath}: {e}")
|
|
40
|
+
print(f"Tip: If {filepath} is an XML file, try providing --ifos")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_plot(
|
|
45
|
+
file_list: List[str],
|
|
46
|
+
output_path: str,
|
|
47
|
+
ifos: Optional[List[str]] = None,
|
|
48
|
+
labels: Optional[List[str]] = None,
|
|
49
|
+
title: Optional[str] = None,
|
|
50
|
+
xlim: Optional[List[float]] = None,
|
|
51
|
+
ylim: Optional[List[float]] = None,
|
|
52
|
+
asd_mode: bool = False,
|
|
53
|
+
sort_time: bool = True,
|
|
54
|
+
):
|
|
55
|
+
"""Generates and saves the PSD comparison plot."""
|
|
56
|
+
|
|
57
|
+
# --- 1. Load Data & Extract Metadata ---
|
|
58
|
+
loaded_items = []
|
|
59
|
+
|
|
60
|
+
for i, fpath in enumerate(file_list):
|
|
61
|
+
# IFO hint from CLI args
|
|
62
|
+
ifo_hint = ifos[i] if ifos and i < len(ifos) else None
|
|
63
|
+
|
|
64
|
+
# Load Series
|
|
65
|
+
series = load_psd(fpath, ifo_hint)
|
|
66
|
+
|
|
67
|
+
# Extract GPS Time from Object Attributes
|
|
68
|
+
gps_time = None
|
|
69
|
+
if series.epoch is not None:
|
|
70
|
+
# gwpy epoch is usually an astropy Time object
|
|
71
|
+
try:
|
|
72
|
+
gps_time = series.epoch.gps
|
|
73
|
+
except AttributeError:
|
|
74
|
+
# Fallback if it's just a number
|
|
75
|
+
gps_time = float(series.epoch)
|
|
76
|
+
|
|
77
|
+
# Extract IFO Name (Prefer Channel Name -> Filename -> Hint)
|
|
78
|
+
detected_ifo = None
|
|
79
|
+
if series.channel:
|
|
80
|
+
# Typical format: "H1:DMT-PSD..."
|
|
81
|
+
cname = str(series.channel)
|
|
82
|
+
if ":" in cname:
|
|
83
|
+
detected_ifo = cname.split(":")[0]
|
|
84
|
+
|
|
85
|
+
if not detected_ifo:
|
|
86
|
+
# Fallback regex on filename
|
|
87
|
+
match = re.search(r"([HLV]1)", os.path.basename(fpath))
|
|
88
|
+
if match:
|
|
89
|
+
detected_ifo = match.group(1)
|
|
90
|
+
|
|
91
|
+
# Final IFO resolution
|
|
92
|
+
final_ifo = ifo_hint if ifo_hint else detected_ifo
|
|
93
|
+
|
|
94
|
+
loaded_items.append(
|
|
95
|
+
{
|
|
96
|
+
"series": series,
|
|
97
|
+
"gps": gps_time,
|
|
98
|
+
"ifo": final_ifo,
|
|
99
|
+
"path": fpath,
|
|
100
|
+
"explicit_label": labels[i] if labels and i < len(labels) else None,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# --- 2. Sort by Time ---
|
|
105
|
+
if sort_time:
|
|
106
|
+
# Sort by GPS, handling None (put at start or end as preferred)
|
|
107
|
+
loaded_items.sort(key=lambda x: x["gps"] if x["gps"] is not None else -1.0)
|
|
108
|
+
|
|
109
|
+
# --- 3. Determine Plot Mode (Drift vs Comparison) ---
|
|
110
|
+
unique_ifos = set(item["ifo"] for item in loaded_items if item["ifo"])
|
|
111
|
+
# If strictly one IFO is found across multiple files, assume Drift Mode
|
|
112
|
+
is_drift_mode = (len(unique_ifos) == 1) and (len(loaded_items) > 1)
|
|
113
|
+
|
|
114
|
+
# --- 4. Setup Plotting ---
|
|
115
|
+
plt.figure(figsize=(12, 8))
|
|
116
|
+
|
|
117
|
+
# Colors
|
|
118
|
+
num_plots = len(loaded_items)
|
|
119
|
+
if is_drift_mode:
|
|
120
|
+
# Sequential (Viridis) for time evolution
|
|
121
|
+
colors = cm.viridis(np.linspace(0, 1, num_plots))
|
|
122
|
+
else:
|
|
123
|
+
# Qualitative (Tab10) for distinct items
|
|
124
|
+
colors = cm.tab10(np.linspace(0, 1, min(num_plots, 10)))
|
|
125
|
+
|
|
126
|
+
# Base Time (Start of sequence)
|
|
127
|
+
t0 = loaded_items[0]["gps"] if loaded_items and loaded_items[0]["gps"] else None
|
|
128
|
+
|
|
129
|
+
for i, item in enumerate(loaded_items):
|
|
130
|
+
series = item["series"]
|
|
131
|
+
|
|
132
|
+
# ASD Transform
|
|
133
|
+
if asd_mode:
|
|
134
|
+
# GWpy propagates units automatically
|
|
135
|
+
series = series**0.5
|
|
136
|
+
series.name = "Amplitude Spectral Density"
|
|
137
|
+
|
|
138
|
+
# Construct Label
|
|
139
|
+
if item["explicit_label"]:
|
|
140
|
+
label = item["explicit_label"]
|
|
141
|
+
elif is_drift_mode and item["gps"] is not None and t0 is not None:
|
|
142
|
+
# Smart Relative Label: "H1 (+16s)"
|
|
143
|
+
dt = item["gps"] - t0
|
|
144
|
+
ifo_str = item["ifo"] if item["ifo"] else "PSD"
|
|
145
|
+
label = f"{ifo_str} (T+{dt:.1f}s)"
|
|
146
|
+
else:
|
|
147
|
+
# Standard Label: "H1 (Channel)" or Filename
|
|
148
|
+
base = item["ifo"] if item["ifo"] else os.path.basename(item["path"])
|
|
149
|
+
# Only append GPS to label if we aren't in smart drift mode
|
|
150
|
+
if item["gps"]:
|
|
151
|
+
base += f" @ {int(item['gps'])}"
|
|
152
|
+
label = base
|
|
153
|
+
|
|
154
|
+
# Plot
|
|
155
|
+
plt.loglog(
|
|
156
|
+
series.frequencies.value,
|
|
157
|
+
series.value,
|
|
158
|
+
label=label,
|
|
159
|
+
alpha=0.8,
|
|
160
|
+
linewidth=1.5,
|
|
161
|
+
color=colors[i % len(colors)],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# --- 5. Styling ---
|
|
165
|
+
plt.xlabel("Frequency [Hz]", fontsize=12)
|
|
166
|
+
|
|
167
|
+
if asd_mode:
|
|
168
|
+
plt.ylabel(r"ASD [$\mathrm{Strain} / \sqrt{\mathrm{Hz}}$]", fontsize=12)
|
|
169
|
+
else:
|
|
170
|
+
plt.ylabel(r"PSD [$\mathrm{Strain}^2 / \mathrm{Hz}$]", fontsize=12)
|
|
171
|
+
|
|
172
|
+
plt.grid(True, which="both", alpha=0.2, ls="-")
|
|
173
|
+
|
|
174
|
+
if xlim:
|
|
175
|
+
plt.xlim(xlim)
|
|
176
|
+
else:
|
|
177
|
+
plt.xlim(10, 2048)
|
|
178
|
+
|
|
179
|
+
if ylim:
|
|
180
|
+
plt.ylim(ylim)
|
|
181
|
+
|
|
182
|
+
if title:
|
|
183
|
+
plt.title(title, fontsize=14)
|
|
184
|
+
else:
|
|
185
|
+
mode = "Drift Evolution" if is_drift_mode else "Noise Comparison"
|
|
186
|
+
if t0 and is_drift_mode:
|
|
187
|
+
mode += f" (Start GPS: {int(t0)})"
|
|
188
|
+
plt.title(mode, fontsize=14)
|
|
189
|
+
|
|
190
|
+
# Legend
|
|
191
|
+
legend = plt.legend(loc="upper right", framealpha=0.9, fontsize=10)
|
|
192
|
+
if is_drift_mode:
|
|
193
|
+
legend.set_title("Time Evolution")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
plt.savefig(output_path, dpi=150, bbox_inches="tight")
|
|
197
|
+
print(f"Plot saved to: {output_path}")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"Error saving plot: {e}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parse_args():
|
|
203
|
+
parser = argparse.ArgumentParser(description="Visualize and Compare GW PSDs.")
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"input_files", nargs="+", type=str, help="Path(s) to input PSD file(s)"
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"-o", "--output", type=str, default="psd_plot.png", help="Output filename"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Plotting Options
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--labels",
|
|
214
|
+
type=str,
|
|
215
|
+
default=None,
|
|
216
|
+
help="Comma-separated labels (overrides auto-detection)",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--ifos",
|
|
220
|
+
type=str,
|
|
221
|
+
default=None,
|
|
222
|
+
help="Comma-separated IFO names (hint for XML parsing)",
|
|
223
|
+
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--asd", action="store_true", help="Plot Amplitude Spectral Density (sqrt(PSD))"
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--no-sort", action="store_true", help="Disable automatic sorting by GPS time"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Ranges
|
|
232
|
+
parser.add_argument("--fmin", type=float, default=10.0, help="Min Frequency")
|
|
233
|
+
parser.add_argument("--fmax", type=float, default=2048.0, help="Max Frequency")
|
|
234
|
+
parser.add_argument("--ymin", type=float, default=None, help="Min Y-axis")
|
|
235
|
+
parser.add_argument("--ymax", type=float, default=None, help="Max Y-axis")
|
|
236
|
+
|
|
237
|
+
parser.add_argument("--title", type=str, default=None, help="Custom plot title")
|
|
238
|
+
|
|
239
|
+
return parser.parse_args()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main():
|
|
243
|
+
args = parse_args()
|
|
244
|
+
|
|
245
|
+
# Process comma-separated lists
|
|
246
|
+
labels = [l.strip() for l in args.labels.split(",")] if args.labels else None
|
|
247
|
+
ifos = [i.strip() for i in args.ifos.split(",")] if args.ifos else None
|
|
248
|
+
|
|
249
|
+
# Handle shell globbing (Python glob if shell didn't expand)
|
|
250
|
+
expanded_files = []
|
|
251
|
+
import glob
|
|
252
|
+
|
|
253
|
+
for f in args.input_files:
|
|
254
|
+
if "*" in f:
|
|
255
|
+
expanded_files.extend(glob.glob(f))
|
|
256
|
+
else:
|
|
257
|
+
expanded_files.append(f)
|
|
258
|
+
|
|
259
|
+
# Remove duplicates and sort for consistency
|
|
260
|
+
expanded_files = sorted(list(set(expanded_files)))
|
|
261
|
+
|
|
262
|
+
if not expanded_files:
|
|
263
|
+
print("No input files found.")
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
266
|
+
make_plot(
|
|
267
|
+
expanded_files,
|
|
268
|
+
output_path=args.output,
|
|
269
|
+
ifos=ifos,
|
|
270
|
+
labels=labels,
|
|
271
|
+
title=args.title,
|
|
272
|
+
xlim=[args.fmin, args.fmax],
|
|
273
|
+
ylim=[args.ymin, args.ymax] if args.ymin and args.ymax else None,
|
|
274
|
+
asd_mode=args.asd,
|
|
275
|
+
sort_time=not args.no_sort,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|