Skip to content

Commit 2a7b181

Browse files
committed
Adding new response test.
1 parent 5dffde1 commit 2a7b181

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import obspy
2+
from obspy.clients.fdsn import Client
3+
from obspy.clients.fdsn.header import URL_MAPPINGS
4+
5+
import pathlib
6+
7+
DATA_PATH = pathlib.Path("./data")
8+
PROVIDERS = sorted(URL_MAPPINGS.keys())
9+
NETWORK = None
10+
STATION = None
11+
12+
# XXX: Overwritten here to only download a subset. For the full
13+
# scale test just comment these lines.
14+
PROVIDERS = ["IRIS"]
15+
NETWORK = "IU"
16+
STATION = "A*"
17+
18+
19+
def download_stationxml_files_for_provider(
20+
provider: str, output_folder: pathlib.Path
21+
) -> None:
22+
def _p(msg):
23+
print(f"Provider '{provider}': {msg}")
24+
25+
output_folder.mkdir(exist_ok=True)
26+
27+
# Get inventory for provider.
28+
client = Client(provider)
29+
_p("Retrieving inventory ...")
30+
try:
31+
inv = client.get_stations(
32+
level="station", format="text", network=NETWORK, station=STATION
33+
)
34+
except Exception as e:
35+
print(f"Failed to initialize client '{provider}' due to: {str(e)}")
36+
return
37+
_p("Done retrieving inventory ...")
38+
39+
# Loop over all stations and retrieve the full response level dictionary.
40+
net_sta = []
41+
for network in inv:
42+
for station in network:
43+
net_sta.append((network.code, station.code))
44+
45+
# Unique list to get rid of station epochs.
46+
net_sta = sorted(set(net_sta))
47+
48+
for _i, (network, station) in enumerate(net_sta):
49+
filename = output_folder / f"{network}_{station}.xml"
50+
if filename.exists():
51+
_p(f"File '{filename} already exists.")
52+
continue
53+
_p(f"Downloading file {_i + 1} of {len(net_sta)}: {filename}")
54+
try:
55+
client.get_stations(
56+
network=network,
57+
station=station,
58+
level="response",
59+
filename=str(filename),
60+
)
61+
except Exception as e:
62+
_p(f"Failed to download '{filename}' due to: {str(e)}")
63+
continue
64+
65+
66+
def main():
67+
for provider in PROVIDERS:
68+
download_stationxml_files_for_provider(
69+
provider=provider, output_folder=DATA_PATH
70+
)
71+
72+
73+
if __name__ == "__main__":
74+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import pathlib
2+
3+
import matplotlib.pyplot as plt
4+
import numpy as np
5+
import obspy
6+
from obspy.core.inventory.response import Response
7+
8+
CACHE_PATH = pathlib.Path("./cache")
9+
DATA_PATH = pathlib.Path("./data")
10+
11+
# Number of frequencies to test at.
12+
N_FREQUENCIES = 100
13+
14+
# TOLERANCES
15+
RTOL = 1e-4
16+
ATOL_AS_FRAC_OF_ABS_MAX = 5e-4
17+
18+
# Collect phase responses here that don't compare favourable to evalresp
19+
# but which have been manually verified to be at least as correct in the
20+
# new scipy implementation.
21+
# This will have to be expanded in the course of this test.
22+
SKIP_VALIDATING_PHASE_REPONSE = ["IU.AFI..UHE", "IU.AFI..UHN", "IU.AFI..UHZ"]
23+
24+
25+
def compare_single_response(channel_id: str, response: Response):
26+
# detect sampling rate from response stages
27+
for stage in response.response_stages[::-1]:
28+
if (
29+
stage.decimation_input_sample_rate is not None
30+
and stage.decimation_factor is not None
31+
):
32+
sampling_rate = stage.decimation_input_sample_rate / stage.decimation_factor
33+
break
34+
else:
35+
# XXX: Something has to be done here.
36+
msg = "Failed to autodetect sampling rate of channel from " "response stages."
37+
raise Exception(msg)
38+
39+
# Compute up to the Nyquist frequency - evalresp's phase usually goes crazy
40+
# afterwards.
41+
FREQUENCIES = np.logspace(-2, np.log10(0.5 * sampling_rate), N_FREQUENCIES)
42+
43+
# Compute for evalresp as well as the scipy response.
44+
try:
45+
eval_resp = response.get_evalresp_response_for_frequencies(
46+
FREQUENCIES, output="VEL"
47+
)
48+
except Exception as e:
49+
print(
50+
" evalresp failed to compute response. Thus no comparision can be"
51+
f"performed. Reason for evalresp failure: {str(e)}"
52+
)
53+
return
54+
55+
scipy_resp = response.get_response(FREQUENCIES, output="VEL")
56+
57+
# Use amplitude and phase for the comparison just because it is more
58+
# intuitive.
59+
eval_resp_amplitude = np.abs(eval_resp)
60+
eval_resp_phase = np.angle(eval_resp)
61+
62+
scipy_resp_amplitude = np.abs(scipy_resp)
63+
scipy_resp_phase = np.angle(scipy_resp)
64+
65+
atol_amplitude = scipy_resp_amplitude.max() * ATOL_AS_FRAC_OF_ABS_MAX
66+
atol_phase = np.abs(scipy_resp_phase).max() * ATOL_AS_FRAC_OF_ABS_MAX
67+
68+
try:
69+
np.testing.assert_allclose(
70+
eval_resp_amplitude,
71+
scipy_resp_amplitude,
72+
rtol=RTOL,
73+
atol=atol_amplitude,
74+
err_msg="amplitude mismatch",
75+
)
76+
# Skip if manually verified.
77+
if channel_id not in SKIP_VALIDATING_PHASE_REPONSE:
78+
np.testing.assert_allclose(
79+
eval_resp_phase,
80+
scipy_resp_phase,
81+
rtol=RTOL,
82+
atol=atol_phase,
83+
err_msg="amplitude mismatch",
84+
)
85+
except Exception as e:
86+
print(f"Failed comparison due to: {str(e)}")
87+
print("Will now produce a plot to help diagnose the issue.")
88+
89+
plt.subplot(411)
90+
plt.title("Amplitude response")
91+
plt.loglog(FREQUENCIES, eval_resp_amplitude, label="Evalresp")
92+
plt.loglog(FREQUENCIES, scipy_resp_amplitude, label="scipy")
93+
plt.legend()
94+
95+
plt.subplot(412)
96+
plt.title("Amplitude response difference")
97+
plt.loglog(FREQUENCIES, eval_resp_amplitude - scipy_resp_amplitude)
98+
99+
plt.subplot(413)
100+
plt.title("Phase response")
101+
plt.semilogx(FREQUENCIES, eval_resp_phase, label="Evalresp")
102+
plt.semilogx(FREQUENCIES, scipy_resp_phase, label="scipy")
103+
plt.legend()
104+
105+
plt.title("Phase response difference")
106+
plt.subplot(414)
107+
plt.semilogx(FREQUENCIES, eval_resp_phase - scipy_resp_phase)
108+
109+
plt.show()
110+
raise e
111+
112+
113+
def test_single_stationxml_file(filename: pathlib.Path):
114+
def _p(msg, indent: int = 0):
115+
print(f"{' ' * indent}File '{filename}': {msg}")
116+
117+
# Simplistic cache to be able to rerun this a lot and fix
118+
# bugs as they appear.
119+
cache_file = CACHE_PATH / filename.name
120+
if cache_file.exists():
121+
_p("Already has been tested. Skipping ...", indent=2)
122+
return
123+
124+
try:
125+
inv = obspy.read_inventory(str(filename))
126+
except Exception as e:
127+
_p(f"Failed to parse due to: {str(e)}")
128+
raise e
129+
130+
all_responses = []
131+
for net in inv:
132+
for sta in net:
133+
for cha in sta:
134+
all_responses.append(
135+
[
136+
net.code,
137+
sta.code,
138+
cha.location_code,
139+
cha.code,
140+
cha.start_date,
141+
cha.end_date,
142+
cha.response,
143+
]
144+
)
145+
for _i, c in enumerate(all_responses):
146+
_p(f"Comparing responses for {c[:-1]} ...", indent=2)
147+
compare_single_response(channel_id=".".join(c[:4]), response=c[-1])
148+
149+
# Finally just touch the cache file so it will be skipped the next run.
150+
cache_file.touch()
151+
152+
153+
def main():
154+
CACHE_PATH.mkdir(exist_ok=True)
155+
all_files = list(DATA_PATH.glob("*.xml"))
156+
for _i, filename in enumerate(all_files):
157+
print(f"Reading StationXML file {_i + 1} of {len(all_files)}: {filename}")
158+
test_single_stationxml_file(filename)
159+
160+
161+
if __name__ == "__main__":
162+
main()
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Brute-Force Response Test
2+
3+
This folder contains a collection of scripts to
4+
5+
(1) Download a large number of StationXML files.
6+
(2) Compare the response calculation in evalresp to a new one in scipy.
7+
8+
The goal is to run this on a significant fraction of the data out there and
9+
document any difference.
10+
11+
## Usage
12+
13+
First edit the script and then run it. By default it only downloads a fairly
14+
small number of files - should be increased down the line.
15+
16+
```bash
17+
python 00_download_data.py
18+
```
19+
20+
Then run the tests on the downloaded files.
21+
22+
```bash
23+
python 01_run_test.py
24+
```
25+
26+
This will require a fair bit of manual work to get to work. The scripts are
27+
designed in a way so they can be rerun and already performed work will be
28+
skipped.

0 commit comments

Comments
 (0)