dssrr 0.1.0__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.
- dssrr/__init__.py +231 -0
- dssrr/cli.py +305 -0
- dssrr/config.py +175 -0
- dssrr/core.py +553 -0
- dssrr/discovery.py +204 -0
- dssrr/gui/__init__.py +33 -0
- dssrr/gui/__main__.py +21 -0
- dssrr/gui/batch_repair.py +782 -0
- dssrr/gui/depulse_dialog.py +2718 -0
- dssrr/gui/export_report_dialog.py +658 -0
- dssrr/gui/i18n.py +413 -0
- dssrr/gui/icons/__init__.py +39 -0
- dssrr/gui/icons/dssrr-128.png +0 -0
- dssrr/gui/icons/dssrr-16.png +0 -0
- dssrr/gui/icons/dssrr-24.png +0 -0
- dssrr/gui/icons/dssrr-256.png +0 -0
- dssrr/gui/icons/dssrr-32.png +0 -0
- dssrr/gui/icons/dssrr-48.png +0 -0
- dssrr/gui/icons/dssrr-64.png +0 -0
- dssrr/gui/icons/dssrr.ico +0 -0
- dssrr/gui/icons/dssrr.png +0 -0
- dssrr/gui/icons/dssrr.svg +24 -0
- dssrr/gui/icons/make_icon.py +209 -0
- dssrr/gui/main.py +846 -0
- dssrr/gui/manual_repair.py +1077 -0
- dssrr/gui/nav_hint.py +182 -0
- dssrr/gui/plot_text_editor.py +505 -0
- dssrr/gui/translations_zh.py +665 -0
- dssrr/gui/viewer.py +3008 -0
- dssrr/i18n.py +122 -0
- dssrr/paths.py +121 -0
- dssrr/repair_lib/__init__.py +38 -0
- dssrr/repair_lib/anomaly_repair/__init__.py +55 -0
- dssrr/repair_lib/anomaly_repair/config.py +26 -0
- dssrr/repair_lib/anomaly_repair/layered_reference_spectrum.py +506 -0
- dssrr/repair_lib/anomaly_repair/moonquake_protected_spike.py +371 -0
- dssrr/repair_lib/anomaly_repair/reference_spectrum.py +42 -0
- dssrr/repair_lib/anomaly_repair/spectral.py +22 -0
- dssrr/repair_lib/anomaly_repair/synthesizer.py +29 -0
- dssrr/repair_lib/anomaly_repair/verifier.py +21 -0
- dssrr/repair_lib/apollo_catalog/gagnepian_2006_catalog.csv +59 -0
- dssrr/repair_lib/apollo_catalog/lognonne_2003_catalog.csv +60 -0
- dssrr/repair_lib/depulse_advanced.py +973 -0
- dssrr/repair_lib/depulse_nn.py +187 -0
- dssrr/repair_lib/e7_detector.py +622 -0
- dssrr/repair_lib/e7_repair_build.py +1719 -0
- dssrr/repair_lib/moonquake_catalog.json +9515 -0
- dssrr/repair_lib/repair_engine.py +673 -0
- dssrr/repair_lib/repair_settings.py +139 -0
- dssrr/repair_lib/report_plot.py +913 -0
- dssrr/spectral.py +209 -0
- dssrr/synthesizer.py +716 -0
- dssrr/verifier.py +309 -0
- dssrr-0.1.0.dist-info/METADATA +483 -0
- dssrr-0.1.0.dist-info/RECORD +59 -0
- dssrr-0.1.0.dist-info/WHEEL +5 -0
- dssrr-0.1.0.dist-info/entry_points.txt +3 -0
- dssrr-0.1.0.dist-info/licenses/LICENSE +21 -0
- dssrr-0.1.0.dist-info/top_level.txt +1 -0
dssrr/__init__.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""DSSRR — Dual-Sided Spectral Reference Reconstruction.
|
|
2
|
+
|
|
3
|
+
Training-free, statistically faithful long-gap repair for planetary and
|
|
4
|
+
seismic time series. DSSRR fills a corrupted or missing interval by fusing
|
|
5
|
+
power-spectral-density estimates taken from the healthy data on **both** sides
|
|
6
|
+
of the gap, synthesising a replacement with realistic waveform texture, and
|
|
7
|
+
blending it into the surrounding trace. No training data and no model fitting
|
|
8
|
+
are required, and the method stays stable for gaps from seconds to over
|
|
9
|
+
90 minutes.
|
|
10
|
+
|
|
11
|
+
Quick start
|
|
12
|
+
-----------
|
|
13
|
+
>>> import numpy as np
|
|
14
|
+
>>> from dssrr import DSSRR
|
|
15
|
+
>>> sr = 6.625 # Apollo long-period sampling rate
|
|
16
|
+
>>> model = DSSRR(sr=sr, reference_sec=300, seed=42)
|
|
17
|
+
>>> repaired = model.repair(data, start=anomaly_start, end=anomaly_end)
|
|
18
|
+
|
|
19
|
+
Package layout
|
|
20
|
+
--------------
|
|
21
|
+
The package is layered so that the scientific core stays lightweight:
|
|
22
|
+
|
|
23
|
+
``dssrr.core`` / ``spectral`` / ``synthesizer`` / ``verifier`` / ``config``
|
|
24
|
+
The algorithm itself. Depends on **numpy + scipy only** — importing
|
|
25
|
+
:mod:`dssrr` never pulls in a GUI toolkit.
|
|
26
|
+
``dssrr.cli``
|
|
27
|
+
Cross-platform command-line batch repair (``dssrr-batch``).
|
|
28
|
+
``dssrr.gui``
|
|
29
|
+
PyQt5 desktop application (``dssrr-gui``): trace viewer, manual repair
|
|
30
|
+
panel and batch runner.
|
|
31
|
+
``dssrr.repair_lib``
|
|
32
|
+
Apollo-specific detection/repair pipeline (E7 detector, multi-level repair
|
|
33
|
+
routing, report plotting) used by the GUI and the batch command.
|
|
34
|
+
``dssrr.paths``
|
|
35
|
+
Canonical locations of the optional user settings / catalogue files.
|
|
36
|
+
``dssrr.i18n``
|
|
37
|
+
Single source of truth for the language of user-facing report text
|
|
38
|
+
(default **English**; override with the ``DSSRR_LANG`` environment
|
|
39
|
+
variable). The system locale is deliberately not consulted.
|
|
40
|
+
``dssrr.gui.i18n`` + ``dssrr.gui.translations_zh``
|
|
41
|
+
Bilingual UI support. A ``QTranslator`` subclass backed by a plain
|
|
42
|
+
``{english: chinese}`` dict makes every existing ``self.tr()`` call site
|
|
43
|
+
translatable without a ``.qm`` toolchain. Still Qt-only — the core
|
|
44
|
+
library never imports it.
|
|
45
|
+
|
|
46
|
+
Citation
|
|
47
|
+
--------
|
|
48
|
+
Yu, S., Li, X. (2026). Dual-Sided Spectral Reference Reconstruction for
|
|
49
|
+
long-duration artifact repair in Apollo lunar seismic records.
|
|
50
|
+
*Seismological Research Letters* (submitted).
|
|
51
|
+
|
|
52
|
+
License: MIT (see ``LICENSE``).
|
|
53
|
+
"""
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
import numpy as np
|
|
56
|
+
from .core import ReferenceSpectrumReplacer
|
|
57
|
+
from .config import get_config
|
|
58
|
+
|
|
59
|
+
__version__ = "0.1.0"
|
|
60
|
+
|
|
61
|
+
def dssrr_gui():
|
|
62
|
+
"""启动 DSSRR 桌面 GUI(查看器 / 手动重处理 / 批量去异常)。
|
|
63
|
+
|
|
64
|
+
惰性导入 dssrr.gui,避免核心库 import 时强制依赖 PyQt5。
|
|
65
|
+
"""
|
|
66
|
+
from .gui import launch_gui
|
|
67
|
+
launch_gui()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dssrr_batch(argv=None):
|
|
71
|
+
"""命令行批量去异常(Linux / macOS / Windows PowerShell 通用)。
|
|
72
|
+
|
|
73
|
+
惰性导入 dssrr.cli,命令行模式无需安装 PyQt5。
|
|
74
|
+
"""
|
|
75
|
+
from .cli import main
|
|
76
|
+
return main(argv)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["DSSRR", "ReferenceSpectrumReplacer", "auto_reference_length",
|
|
80
|
+
"dssrr_gui", "dssrr_batch", "__version__"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DSSRR:
|
|
84
|
+
"""High-level convenience wrapper around :class:`ReferenceSpectrumReplacer`.
|
|
85
|
+
|
|
86
|
+
Use this when you just want to repair a trace. For full control over the
|
|
87
|
+
configuration dictionary (spectral estimation, synthesis, boundary joining
|
|
88
|
+
and verification thresholds), use :class:`ReferenceSpectrumReplacer`
|
|
89
|
+
directly.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
sr : float
|
|
94
|
+
Sampling rate in Hz. Must be positive. Apollo long-period data is
|
|
95
|
+
6.625 Hz.
|
|
96
|
+
reference_sec : float, optional
|
|
97
|
+
Default reference window length **per side**, in seconds (default
|
|
98
|
+
300.0). For long gaps prefer :func:`auto_reference_length`.
|
|
99
|
+
seed : int, optional
|
|
100
|
+
Random seed used for deterministic synthesis (default 42). Pass
|
|
101
|
+
``None`` at call time for fresh randomness — note that the result is
|
|
102
|
+
then not reproducible.
|
|
103
|
+
quantize : bool, optional
|
|
104
|
+
If ``True``, round the repaired samples to integers using
|
|
105
|
+
round-half-to-even. Enable this only for traces stored as raw integer
|
|
106
|
+
counts (e.g. the Apollo archive); leave it ``False`` for physical-unit
|
|
107
|
+
data such as velocity or displacement. Can be overridden per call.
|
|
108
|
+
config : dict, optional
|
|
109
|
+
Configuration overrides merged onto :data:`dssrr.config.DEFAULT_CONFIG`.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
>>> import numpy as np
|
|
114
|
+
>>> from dssrr import DSSRR, auto_reference_length
|
|
115
|
+
>>> sr = 6.625
|
|
116
|
+
>>> data = 500.0 + np.random.default_rng(0).normal(0, 3, 20000)
|
|
117
|
+
>>> data[6000:9001] = 0.0 # a 7.5-minute dropout
|
|
118
|
+
>>> ref = auto_reference_length(3001, sr) # 226.5 s per side
|
|
119
|
+
>>> model = DSSRR(sr=sr, reference_sec=ref, seed=42)
|
|
120
|
+
>>> repaired = model.repair(data, 6000, 9000)
|
|
121
|
+
>>> repaired.shape == data.shape
|
|
122
|
+
True
|
|
123
|
+
|
|
124
|
+
Notes
|
|
125
|
+
-----
|
|
126
|
+
DSSRR reproduces the **second-order statistics** of the surrounding trace
|
|
127
|
+
(power spectrum, autocorrelation, energy). The original phase inside the
|
|
128
|
+
gap is not recoverable, so a one-off transient that happened during the
|
|
129
|
+
dropout cannot be reconstructed.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, sr: float, reference_sec: float = 300.0,
|
|
133
|
+
seed: int = 42, quantize: bool = False,
|
|
134
|
+
config: dict | None = None):
|
|
135
|
+
if sr <= 0:
|
|
136
|
+
raise ValueError("Sampling rate must be positive")
|
|
137
|
+
self.sr = float(sr)
|
|
138
|
+
self.reference_sec = float(reference_sec)
|
|
139
|
+
self.seed = seed
|
|
140
|
+
self.quantize = bool(quantize)
|
|
141
|
+
self._replacer = ReferenceSpectrumReplacer(sr=sr, config=config)
|
|
142
|
+
|
|
143
|
+
def repair(self, data: np.ndarray, start: int, end: int,
|
|
144
|
+
*, reference_before_sec: float | None = None,
|
|
145
|
+
reference_after_sec: float | None = None,
|
|
146
|
+
safety_gap_sec: float | None = None,
|
|
147
|
+
seed: int | None = None,
|
|
148
|
+
quantize: bool | None = None) -> np.ndarray:
|
|
149
|
+
"""Repair ``data[start:end + 1]`` in place-free fashion.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
data : numpy.ndarray
|
|
154
|
+
1-D waveform. Never modified in place — a repaired copy is
|
|
155
|
+
returned, and every sample outside ``[start, end]`` is copied
|
|
156
|
+
through bit-identically.
|
|
157
|
+
start, end : int
|
|
158
|
+
Inclusive sample indices of the anomaly core.
|
|
159
|
+
reference_before_sec, reference_after_sec : float, optional
|
|
160
|
+
Reference window length per side. Default to ``reference_sec``.
|
|
161
|
+
safety_gap_sec : float, optional
|
|
162
|
+
Guard interval between the anomaly core and each reference window,
|
|
163
|
+
in seconds. ``None`` (the default) defers to the replacer's own
|
|
164
|
+
default so there is a single source of truth for this value.
|
|
165
|
+
seed : int, optional
|
|
166
|
+
Override the instance seed. ``None`` means "use the instance
|
|
167
|
+
seed"; to request fresh randomness pass ``seed=None`` to the
|
|
168
|
+
constructor instead.
|
|
169
|
+
quantize : bool, optional
|
|
170
|
+
Override the instance ``quantize`` flag for this call only.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
numpy.ndarray
|
|
175
|
+
A repaired copy of ``data`` with the same shape and dtype layout
|
|
176
|
+
(float; integer input becomes integer again when ``quantize``).
|
|
177
|
+
"""
|
|
178
|
+
rb = self.reference_sec if reference_before_sec is None else reference_before_sec
|
|
179
|
+
ra = self.reference_sec if reference_after_sec is None else reference_after_sec
|
|
180
|
+
s = self.seed if seed is None else seed
|
|
181
|
+
q = self.quantize if quantize is None else bool(quantize)
|
|
182
|
+
kwargs = dict(
|
|
183
|
+
reference_before_sec=rb,
|
|
184
|
+
reference_after_sec=ra,
|
|
185
|
+
random_seed=s,
|
|
186
|
+
quantize=q,
|
|
187
|
+
)
|
|
188
|
+
if safety_gap_sec is not None:
|
|
189
|
+
kwargs["reference_gap_sec"] = float(safety_gap_sec)
|
|
190
|
+
repaired, _ = self._replacer.replace(
|
|
191
|
+
np.asarray(data, dtype=float), int(start), int(end), **kwargs)
|
|
192
|
+
return repaired
|
|
193
|
+
|
|
194
|
+
def repair_with_report(self, data: np.ndarray, start: int, end: int,
|
|
195
|
+
**kwargs) -> tuple[np.ndarray, dict]:
|
|
196
|
+
"""Repair and also return the full provenance / quality report.
|
|
197
|
+
|
|
198
|
+
Every :meth:`ReferenceSpectrumReplacer.replace` keyword is forwarded,
|
|
199
|
+
so this is the way to reach ``reference_gap_sec``, ``quantize``,
|
|
200
|
+
``random_seed`` and friends while still using the convenience wrapper.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
(numpy.ndarray, dict)
|
|
205
|
+
The repaired trace and a report describing the reference windows
|
|
206
|
+
used, baseline alignment, verification metrics and known
|
|
207
|
+
limitations.
|
|
208
|
+
"""
|
|
209
|
+
return self._replacer.replace(np.asarray(data, dtype=float),
|
|
210
|
+
int(start), int(end), **kwargs)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def auto_reference_length(anomaly_samples: int, sr: float,
|
|
214
|
+
min_sec: float = 120.0, max_sec: float = 600.0) -> float:
|
|
215
|
+
"""Pick a reference-window length from the anomaly duration.
|
|
216
|
+
|
|
217
|
+
Returns ``clip(anomaly_duration / 2, min_sec, max_sec)`` seconds — the
|
|
218
|
+
protocol used for the DSSRR paper (120–600 s for Apollo data). Half the
|
|
219
|
+
anomaly length is enough for a stable spectral estimate, while the bounds
|
|
220
|
+
keep the window short enough to stay locally stationary.
|
|
221
|
+
|
|
222
|
+
Examples
|
|
223
|
+
--------
|
|
224
|
+
>>> auto_reference_length(int(60 * 6.625), 6.625) # 60 s anomaly
|
|
225
|
+
120.0
|
|
226
|
+
>>> auto_reference_length(int(2000 * 6.625), 6.625) # very long anomaly
|
|
227
|
+
600.0
|
|
228
|
+
"""
|
|
229
|
+
anom_sec = float(anomaly_samples) / float(sr)
|
|
230
|
+
return float(np.clip(anom_sec / 2.0, min_sec, max_sec))
|
|
231
|
+
|
dssrr/cli.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DSSRR command-line batch repair (``dssrr-batch``).
|
|
3
|
+
|
|
4
|
+
Cross-platform (Linux / macOS / Windows PowerShell) entry point for repairing a
|
|
5
|
+
whole archive month by month. It runs **the same pipeline as the desktop GUI's
|
|
6
|
+
Batch Repair page**, but without importing PyQt, so it works on headless
|
|
7
|
+
servers.
|
|
8
|
+
|
|
9
|
+
Pipeline
|
|
10
|
+
--------
|
|
11
|
+
::
|
|
12
|
+
|
|
13
|
+
scan scope -> E7 anomaly detection -> per-file multi-level repair
|
|
14
|
+
-> moonquake-catalogue protection -> write catalog CSV
|
|
15
|
+
-> automatic verification -> summary JSON
|
|
16
|
+
|
|
17
|
+
Directory convention::
|
|
18
|
+
|
|
19
|
+
<raw_root>/1976/01/XA.S12.19760113.mseed # input, read-only
|
|
20
|
+
<fix_root>/1976/01/XA.S12.19760113.mseed # output, mirror structure
|
|
21
|
+
<catalog_dir>/E7_catalog_197601.csv # per-month repair detail
|
|
22
|
+
<catalog_dir>/verify_197601.json # per-month verification
|
|
23
|
+
<catalog_dir>/batch_summary.json # overall summary
|
|
24
|
+
|
|
25
|
+
Usage
|
|
26
|
+
-----
|
|
27
|
+
::
|
|
28
|
+
|
|
29
|
+
dssrr-batch --raw-root /data/apollo/raw --fix-root /data/apollo/repair \\
|
|
30
|
+
--stations S12,S15 --d0 19760101 --d1 19760229
|
|
31
|
+
|
|
32
|
+
Exit status is ``0`` when verification passes for every repaired file, and
|
|
33
|
+
``1`` when any file failed or the arguments were invalid. See ``docs/CLI.md``
|
|
34
|
+
for the full option list and worked examples.
|
|
35
|
+
|
|
36
|
+
Programmatic use: :func:`quick_scan` and :func:`batch_repair` are importable
|
|
37
|
+
directly and take plain Python arguments.
|
|
38
|
+
|
|
39
|
+
Dependencies: numpy, obspy, pandas (no PyQt5).
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import sys
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
from . import discovery
|
|
50
|
+
|
|
51
|
+
DEFAULT_SR = 6.625
|
|
52
|
+
SEC_PER_FILE = 0.9 # 1976-01 measured baseline (same estimate as the GUI)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ----------------------------------------------------------------------
|
|
56
|
+
# 文件扫描 —— 实现见 dssrr.discovery(与 GUI batch_repair 共用同一份规则)
|
|
57
|
+
# ----------------------------------------------------------------------
|
|
58
|
+
_iter_scope_files = discovery.iter_scope_files
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def quick_scan(raw_root, stations, d0, d1):
|
|
62
|
+
"""快速统计:文件数、按年月、总字节、预计耗时(不读文件内容)。"""
|
|
63
|
+
n, nbytes = 0, 0
|
|
64
|
+
by_ym = {}
|
|
65
|
+
for f, y, m in _iter_scope_files(raw_root, stations, d0, d1):
|
|
66
|
+
n += 1
|
|
67
|
+
by_ym[f"{y:04d}-{m:02d}"] = by_ym.get(f"{y:04d}-{m:02d}", 0) + 1
|
|
68
|
+
try:
|
|
69
|
+
nbytes += os.path.getsize(f)
|
|
70
|
+
except OSError:
|
|
71
|
+
pass
|
|
72
|
+
return {
|
|
73
|
+
"files": n, "bytes": nbytes, "by_ym": by_ym,
|
|
74
|
+
"est_sec": n * SEC_PER_FILE,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ----------------------------------------------------------------------
|
|
79
|
+
# 批处理主流程(等价 GUI BatchWorker.run,无 Qt 信号)
|
|
80
|
+
# ----------------------------------------------------------------------
|
|
81
|
+
def batch_repair(raw_root, fix_root, stations, d0, d1,
|
|
82
|
+
catalog_dir=None, use_catalog=True,
|
|
83
|
+
det_kwargs=None, params=None, sr=DEFAULT_SR,
|
|
84
|
+
log=None, stop_flag=None):
|
|
85
|
+
"""执行批量去异常并返回汇总 dict。stop_flag 为可选的 callable() -> bool。"""
|
|
86
|
+
if log is None:
|
|
87
|
+
log = lambda msg: print(msg) # noqa: E731
|
|
88
|
+
from dssrr.repair_lib.e7_repair_build import (repair_one_file,
|
|
89
|
+
write_catalog_csv,
|
|
90
|
+
verify_file)
|
|
91
|
+
from dssrr.repair_lib.e7_detector import E7Detector
|
|
92
|
+
|
|
93
|
+
det = E7Detector(sr=sr, **(det_kwargs or {}))
|
|
94
|
+
if catalog_dir is None:
|
|
95
|
+
catalog_dir = os.path.join(fix_root, "catalogs")
|
|
96
|
+
|
|
97
|
+
files = list(_iter_scope_files(raw_root, stations, d0, d1))
|
|
98
|
+
total = len(files)
|
|
99
|
+
log(f"[scope] {total} file(s), stations={','.join(stations) or 'ALL'}, "
|
|
100
|
+
f"{d0}~{d1}")
|
|
101
|
+
if total == 0:
|
|
102
|
+
return {"files": 0, "repaired": 0, "gaps": 0, "errors": [],
|
|
103
|
+
"months": [], "verify": None, "elapsed": 0.0}
|
|
104
|
+
|
|
105
|
+
t0 = time.time()
|
|
106
|
+
summary = {"files": total, "repaired": 0, "gaps": 0, "errors": [],
|
|
107
|
+
"months": [], "verify": None, "elapsed": 0.0}
|
|
108
|
+
groups = {}
|
|
109
|
+
for f, y, m in files:
|
|
110
|
+
groups.setdefault(f"{y:04d}{m:02d}", []).append(f)
|
|
111
|
+
|
|
112
|
+
for i, ym in enumerate(sorted(groups), 1):
|
|
113
|
+
if stop_flag and stop_flag():
|
|
114
|
+
log("[stop] user requested stop")
|
|
115
|
+
break
|
|
116
|
+
out_ym = os.path.join(fix_root, ym[:4], ym[4:6])
|
|
117
|
+
rows = []
|
|
118
|
+
log(f"[{i}/{len(groups)}] month {ym}: {len(groups[ym])} file(s)")
|
|
119
|
+
for f in sorted(groups[ym]):
|
|
120
|
+
if stop_flag and stop_flag():
|
|
121
|
+
break
|
|
122
|
+
try:
|
|
123
|
+
nrep, ngap = repair_one_file(
|
|
124
|
+
f, det, out_ym, verbose=False, catalog_rows=rows,
|
|
125
|
+
params=params, use_catalog=use_catalog)
|
|
126
|
+
summary["repaired"] += nrep
|
|
127
|
+
summary["gaps"] += ngap
|
|
128
|
+
log(f" repaired {os.path.basename(f)} "
|
|
129
|
+
f"(rep={nrep} gap={ngap})")
|
|
130
|
+
except Exception as ex:
|
|
131
|
+
summary["errors"].append(
|
|
132
|
+
(os.path.basename(f), str(ex)[:120]))
|
|
133
|
+
log(f" ERR {os.path.basename(f)}: {ex}")
|
|
134
|
+
if rows and catalog_dir:
|
|
135
|
+
try:
|
|
136
|
+
os.makedirs(catalog_dir, exist_ok=True)
|
|
137
|
+
cpath = os.path.join(catalog_dir, f"E7_catalog_{ym}.csv")
|
|
138
|
+
write_catalog_csv(cpath, rows)
|
|
139
|
+
log(f"[catalog] {ym}: {len(rows)} row(s) -> {cpath}")
|
|
140
|
+
except Exception as ex:
|
|
141
|
+
log(f"[catalog] {ym} ERR: {ex}")
|
|
142
|
+
summary["months"].append(ym)
|
|
143
|
+
|
|
144
|
+
# 自动验收:只验收本次处理过的文件
|
|
145
|
+
log("[verify] verifying repaired files...")
|
|
146
|
+
try:
|
|
147
|
+
ver = {"ok": True, "n_files": 0, "n_fail": 0, "n_skip": 0,
|
|
148
|
+
"by_month": {}}
|
|
149
|
+
for ym in sorted(groups):
|
|
150
|
+
r_raw = os.path.join(raw_root, ym[:4], ym[4:6])
|
|
151
|
+
r_fix = os.path.join(fix_root, ym[:4], ym[4:6])
|
|
152
|
+
ym_files, ym_fail = 0, 0
|
|
153
|
+
for f in sorted(groups[ym]):
|
|
154
|
+
fb = os.path.basename(f)
|
|
155
|
+
fix_path = os.path.join(r_fix, fb)
|
|
156
|
+
if not os.path.exists(fix_path):
|
|
157
|
+
ver["n_skip"] += 1
|
|
158
|
+
log(f" SKIP {fb}: no repaired file")
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
rep = verify_file(f, fix_path, det,
|
|
162
|
+
use_catalog=use_catalog)
|
|
163
|
+
ver["n_files"] += 1
|
|
164
|
+
ym_files += 1
|
|
165
|
+
if not rep.get("ok"):
|
|
166
|
+
ver["n_fail"] += 1
|
|
167
|
+
ym_fail += 1
|
|
168
|
+
ver["ok"] = False
|
|
169
|
+
log(f" FAIL {fb}: "
|
|
170
|
+
f"{'; '.join(rep.get('issues', []))}")
|
|
171
|
+
except Exception as ex:
|
|
172
|
+
ver["n_fail"] += 1
|
|
173
|
+
ym_fail += 1
|
|
174
|
+
ver["ok"] = False
|
|
175
|
+
log(f" ERR {fb}: {ex}")
|
|
176
|
+
ver["by_month"][ym] = {"ok": ym_fail == 0, "n_files": ym_files,
|
|
177
|
+
"n_fail": ym_fail}
|
|
178
|
+
if catalog_dir:
|
|
179
|
+
try:
|
|
180
|
+
os.makedirs(catalog_dir, exist_ok=True)
|
|
181
|
+
vpath = os.path.join(
|
|
182
|
+
catalog_dir,
|
|
183
|
+
"verify_" + "_".join(sorted(summary["months"]))[:12]
|
|
184
|
+
+ ".json")
|
|
185
|
+
with open(vpath, "w", encoding="utf-8") as f:
|
|
186
|
+
json.dump(ver, f, ensure_ascii=False, indent=1)
|
|
187
|
+
log(f"[verify] json -> {vpath}")
|
|
188
|
+
except Exception as ex:
|
|
189
|
+
log(f"[verify] json ERR: {ex}")
|
|
190
|
+
summary["verify"] = ver
|
|
191
|
+
except Exception as ex:
|
|
192
|
+
summary["verify"] = {"ok": False, "error": str(ex)[:200]}
|
|
193
|
+
|
|
194
|
+
summary["elapsed"] = time.time() - t0
|
|
195
|
+
log(f"[done] repaired={summary['repaired']} gaps={summary['gaps']} "
|
|
196
|
+
f"errors={len(summary['errors'])} elapsed={summary['elapsed']:.1f}s "
|
|
197
|
+
f"verify_ok={summary['verify'].get('ok') if summary['verify'] else None}")
|
|
198
|
+
return summary
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ----------------------------------------------------------------------
|
|
202
|
+
# 参数解析 / 入口
|
|
203
|
+
# ----------------------------------------------------------------------
|
|
204
|
+
def _positive_date(v):
|
|
205
|
+
v = v.strip()
|
|
206
|
+
if not (len(v) == 8 and v.isdigit()):
|
|
207
|
+
raise argparse.ArgumentTypeError(f"bad YYYYMMDD: {v!r}")
|
|
208
|
+
return int(v)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run(args):
|
|
212
|
+
if not args.raw_root or not os.path.isdir(args.raw_root):
|
|
213
|
+
sys.exit(f"[error] raw root not found: {args.raw_root!r}")
|
|
214
|
+
stations = [s.strip() for s in (args.stations or "").split(",")
|
|
215
|
+
if s.strip()]
|
|
216
|
+
d0, d1 = sorted((args.d0, args.d1))
|
|
217
|
+
r = quick_scan(args.raw_root, stations, d0, d1)
|
|
218
|
+
print(f"[scan] {r['files']} file(s), {r['bytes']/1024**3:.2f} GB, "
|
|
219
|
+
f"{len(r['by_ym'])} month(s), est. {r['est_sec']/60:.1f} min")
|
|
220
|
+
|
|
221
|
+
det_kwargs, params = {}, None
|
|
222
|
+
if args.settings:
|
|
223
|
+
try:
|
|
224
|
+
from dssrr.paths import settings_read_path
|
|
225
|
+
from dssrr.repair_lib import repair_settings as RS
|
|
226
|
+
s = RS.load_settings()
|
|
227
|
+
det_kwargs = dict(s.get("detect", {}) or {})
|
|
228
|
+
params = dict(s.get("repair", {}) or {})
|
|
229
|
+
print(f"[settings] loaded user settings "
|
|
230
|
+
f"({settings_read_path()})")
|
|
231
|
+
except Exception as ex:
|
|
232
|
+
print(f"[warning] settings load failed, using defaults: {ex}")
|
|
233
|
+
|
|
234
|
+
summary = batch_repair(
|
|
235
|
+
args.raw_root, args.fix_root, stations, d0, d1,
|
|
236
|
+
catalog_dir=args.catalog_dir, use_catalog=not args.no_catalog,
|
|
237
|
+
det_kwargs=det_kwargs, params=params, sr=args.sr)
|
|
238
|
+
|
|
239
|
+
# 汇总落盘
|
|
240
|
+
os.makedirs(args.catalog_dir, exist_ok=True) if args.catalog_dir else None
|
|
241
|
+
out_json = args.summary_json
|
|
242
|
+
with open(out_json, "w", encoding="utf-8") as f:
|
|
243
|
+
json.dump(summary, f, ensure_ascii=False, indent=1,
|
|
244
|
+
default=lambda o: o if not hasattr(o, "isoformat") else
|
|
245
|
+
str(o))
|
|
246
|
+
print(f"[summary] -> {out_json}")
|
|
247
|
+
ok = summary["verify"] and summary["verify"].get("ok")
|
|
248
|
+
if ok:
|
|
249
|
+
print("[RESULT] VERIFY OK")
|
|
250
|
+
else:
|
|
251
|
+
print("[RESULT] VERIFY FAIL "
|
|
252
|
+
f"(fail={summary['verify'].get('n_fail', '?')} "
|
|
253
|
+
f"skip={summary['verify'].get('n_skip', '?')})" if ok is False
|
|
254
|
+
else "[RESULT] VERIFY FAIL (see json)")
|
|
255
|
+
return 0 if ok else 1
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def build_parser():
|
|
259
|
+
p = argparse.ArgumentParser(
|
|
260
|
+
prog="dssrr batch-repair",
|
|
261
|
+
description="DSSRR 批量去异常命令行工具(Linux/PowerShell 通用)")
|
|
262
|
+
p.add_argument("--raw-root", required=True,
|
|
263
|
+
help="raw 库根目录;目录结构不限,文件名需为 "
|
|
264
|
+
"NET.STA.YYYYMMDD.mseed 或 "
|
|
265
|
+
"NET.STA.LOC.CHA.START-END.mseed")
|
|
266
|
+
p.add_argument("--fix-root", required=True,
|
|
267
|
+
help="修复库输出根目录(自动创建)")
|
|
268
|
+
p.add_argument("--stations", default="",
|
|
269
|
+
help="逗号分隔台站列表,如 S12,S15;缺省=全部")
|
|
270
|
+
p.add_argument("--d0", type=_positive_date, default=19760101,
|
|
271
|
+
help="开始日期 YYYYMMDD")
|
|
272
|
+
p.add_argument("--d1", type=_positive_date, default=19760201,
|
|
273
|
+
help="结束日期 YYYYMMDD")
|
|
274
|
+
p.add_argument("--catalog-dir", default=None,
|
|
275
|
+
help="catalog/verify 输出目录;缺省 = <fix-root>/catalogs")
|
|
276
|
+
p.add_argument("--no-catalog", action="store_true",
|
|
277
|
+
help="关闭月震目录保护(检测器判定事件全部转 repair)")
|
|
278
|
+
p.add_argument("--settings", action="store_true",
|
|
279
|
+
help="读取用户设置 ~/.dssrr_settings.json "
|
|
280
|
+
"(检测/修复参数);缺省=内置默认")
|
|
281
|
+
p.add_argument("--sr", type=float, default=DEFAULT_SR,
|
|
282
|
+
help=f"采样率 Hz(缺省 {DEFAULT_SR})")
|
|
283
|
+
p.add_argument("--summary-json", default=None,
|
|
284
|
+
help="汇总 JSON 输出路径;缺省 = <catalog-dir>/"
|
|
285
|
+
"batch_summary.json")
|
|
286
|
+
return p
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main(argv=None):
|
|
290
|
+
args = build_parser().parse_args(argv)
|
|
291
|
+
if args.summary_json is None:
|
|
292
|
+
cat = args.catalog_dir or os.path.join(args.fix_root, "catalogs")
|
|
293
|
+
args.summary_json = os.path.join(cat, "batch_summary.json")
|
|
294
|
+
if args.catalog_dir is None:
|
|
295
|
+
args.catalog_dir = os.path.join(args.fix_root, "catalogs")
|
|
296
|
+
try:
|
|
297
|
+
rc = _run(args)
|
|
298
|
+
except KeyboardInterrupt:
|
|
299
|
+
print("\n[interrupted by user]", file=sys.stderr)
|
|
300
|
+
rc = 130
|
|
301
|
+
sys.exit(rc)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
main()
|