sgn-drift 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sgn-drift
3
- Version: 0.1.0
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
@@ -1,12 +1,14 @@
1
1
  sgndrift/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sgndrift/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
2
+ sgndrift/_version.py,sha256=m8HxkqoKGw_wAJtc4ZokpJKNLXqp4zwnNhbnfDtro7w,704
3
3
  sgndrift/bin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  sgndrift/bin/estimate_drift.py,sha256=qVp-PacdNRUAEWOrHlvzP-whMRCB_zhteZZDE1Ld_9g,8822
5
+ sgndrift/bin/estimate_psd.py,sha256=Nb-12CzE1s2uOvFzsB3O8z-2wm72BuEOhvOVfq74n1U,6728
5
6
  sgndrift/bin/plot_drift.py,sha256=EYIhluD0asFWoYmLfpu2vsmU5TDHtjmPjCNuY2tvYLI,4746
6
7
  sgndrift/bin/plot_drift_comparison.py,sha256=0VknMCS1-szBk5trTARUYvYWEXrid-FnyOf0yM1_lnw,6355
7
8
  sgndrift/bin/plot_drift_super.py,sha256=W-OgqfL8_UFDKxyNhNe3ahXteQllu3oHPyidYcJd6Kc,7934
8
9
  sgndrift/bin/plot_drift_super_comp.py,sha256=lcHqw4w0xKfTW1DX5pAVOGM3mAlxfc9Xsz12fjlQlTQ,12222
9
10
  sgndrift/bin/plot_drift_time.py,sha256=EdLkzUe4Exsi6OJDQYLmY8PH1wSHLoRv5q3ZwKyDS8g,5929
11
+ sgndrift/bin/plot_psd.py,sha256=SRkqZrJqZhbn8rRVPu5vShNMGm2VVQi1Y5-MzBGEhPo,8635
10
12
  sgndrift/psd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
13
  sgndrift/psd/drift.py,sha256=3v0UlLc3yQLSJwLHJXQo5Hy1wRM7wV8hkMiiSGtpY8o,2413
12
14
  sgndrift/psd/estimators.py,sha256=UdI7SaUOEiQeXbBzWNfY3C0w0tS8NWMzJHzULJW30Ck,4280
@@ -15,8 +17,8 @@ sgndrift/sinks/drift_sink.py,sha256=PHspvH9LupSmjLWjavvsk2a3TfU5jYXUMN0TV9lTdjs,
15
17
  sgndrift/transforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
18
  sgndrift/transforms/drift.py,sha256=bbjmrW1q8awkb-oBpeWo94M-pGxHYN88iOKrkbQHL2A,4727
17
19
  sgndrift/transforms/psd.py,sha256=6LABGSUpbOE4nKzHEWybHT3ronnuUgLuV5tI46txn34,5427
18
- sgn_drift-0.1.0.dist-info/METADATA,sha256=-5mp3zaZIktyCf2Ap6ljRQXEGXwd0QMIWaUFqQhtoRw,2828
19
- sgn_drift-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
- sgn_drift-0.1.0.dist-info/entry_points.txt,sha256=emRR36dQDFCkTxwuWEMnN-TeNNTrnlJ-HFeJEw_37vY,362
21
- sgn_drift-0.1.0.dist-info/top_level.txt,sha256=xmogf6bL8sl_VbrQ6pPMXLdY967VtYYUtFzIg_U3Rq4,9
22
- sgn_drift-0.1.0.dist-info/RECORD,,
20
+ sgn_drift-0.1.1.dist-info/METADATA,sha256=AaZAytDaFP4AMNxOwK4CA6qc393f6itdjUB0j9YQgz8,2828
21
+ sgn_drift-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ sgn_drift-0.1.1.dist-info/entry_points.txt,sha256=3duQ4GjWXADFIL0L_sXEjzhZS2-Z9wasOuMyRtnVO3c,457
23
+ sgn_drift-0.1.1.dist-info/top_level.txt,sha256=xmogf6bL8sl_VbrQ6pPMXLdY967VtYYUtFzIg_U3Rq4,9
24
+ sgn_drift-0.1.1.dist-info/RECORD,,
@@ -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
sgndrift/_version.py CHANGED
@@ -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.0'
32
- __version_tuple__ = version_tuple = (0, 1, 0)
31
+ __version__ = version = '0.1.1'
32
+ __version_tuple__ = version_tuple = (0, 1, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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()