DASPy-toolbox 1.0.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.
- DASPy_toolbox-1.0.0.dist-info/LICENSE.txt +1 -0
- DASPy_toolbox-1.0.0.dist-info/METADATA +85 -0
- DASPy_toolbox-1.0.0.dist-info/RECORD +49 -0
- DASPy_toolbox-1.0.0.dist-info/WHEEL +5 -0
- DASPy_toolbox-1.0.0.dist-info/entry_points.txt +2 -0
- DASPy_toolbox-1.0.0.dist-info/top_level.txt +1 -0
- daspy/__init__.py +4 -0
- daspy/advanced_tools/__init__.py +0 -0
- daspy/advanced_tools/channel.py +354 -0
- daspy/advanced_tools/decomposition.py +165 -0
- daspy/advanced_tools/denoising.py +276 -0
- daspy/advanced_tools/fdct.py +789 -0
- daspy/advanced_tools/strain2vel.py +245 -0
- daspy/basic_tools/__init__.py +0 -0
- daspy/basic_tools/filter.py +257 -0
- daspy/basic_tools/freqattributes.py +117 -0
- daspy/basic_tools/preprocessing.py +238 -0
- daspy/basic_tools/visualization.py +186 -0
- daspy/core/__init__.py +4 -0
- daspy/core/collection.py +279 -0
- daspy/core/dasdatetime.py +72 -0
- daspy/core/example.pkl +0 -0
- daspy/core/make_example.py +32 -0
- daspy/core/read.py +544 -0
- daspy/core/section.py +1319 -0
- daspy/core/write.py +282 -0
- daspy/seismic_detection/__init__.py +1 -0
- daspy/seismic_detection/calc_travel_time.py +23 -0
- daspy/seismic_detection/core.py +119 -0
- daspy/seismic_detection/detection.py +12 -0
- daspy/seismic_detection/gamma/__init__.py +13 -0
- daspy/seismic_detection/gamma/_base.py +549 -0
- daspy/seismic_detection/gamma/_bayesian_mixture.py +875 -0
- daspy/seismic_detection/gamma/_gaussian_mixture.py +866 -0
- daspy/seismic_detection/gamma/app.py +192 -0
- daspy/seismic_detection/gamma/seismic_ops.py +478 -0
- daspy/seismic_detection/gamma/utils.py +512 -0
- daspy/seismic_detection/location.py +266 -0
- daspy/seismic_detection/magnitude.py +43 -0
- daspy/seismic_detection/phase_picking.py +67 -0
- daspy/structure_imaging/__init__.py +0 -0
- daspy/structure_imaging/ambient_noise.py +4 -0
- daspy/structure_imaging/dispersion.py +27 -0
- daspy/structure_imaging/fault_zone.py +59 -0
- daspy/structure_imaging/inversion.py +6 -0
- daspy/traffic_monitoring/JamDetection.py +6 -0
- daspy/traffic_monitoring/SpeedMeasurement.py +6 -0
- daspy/traffic_monitoring/VehicleDetection.py +6 -0
- daspy/traffic_monitoring/__init__.py +0 -0
daspy/core/collection.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Purpose: Module for handling Collection objects.
|
|
2
|
+
# Author: Minzhe Hu
|
|
3
|
+
# Date: 2024.11.1
|
|
4
|
+
# Email: hmz2018@mail.ustc.edu.cn
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
import numpy as np
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
from glob import glob
|
|
10
|
+
from daspy.core.read import read
|
|
11
|
+
from daspy.core.dasdatetime import DASDateTime
|
|
12
|
+
from daspy.basic_tools.preprocessing import cosine_taper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
cascade_method = ['time_integration', 'time_differential', 'downsampling',
|
|
16
|
+
'bandpass', 'bandstop', 'lowpass', 'highpass',
|
|
17
|
+
'lowpass_cheby_2']
|
|
18
|
+
|
|
19
|
+
class Collection(object):
|
|
20
|
+
def __init__(self, fpath, ftype=None, flength=None, meta_from_file=True,
|
|
21
|
+
timeinfo_format=None, timeinfo_from_basename=True, **kwargs):
|
|
22
|
+
"""
|
|
23
|
+
:param fpath: str or Sequence of str. File path(s) containing data.
|
|
24
|
+
:param ftype: None or str. None for automatic detection, or 'pkl',
|
|
25
|
+
'pickle', 'tdms', 'h5', 'hdf5', 'segy', 'sgy', 'npy'.
|
|
26
|
+
:param flength: float. The duration of a single file in senconds.
|
|
27
|
+
:param meta_from_file: bool or 'all'. False for manually set dt, dx, fs
|
|
28
|
+
and gauge_length. True for extracting dt, dx, fs and gauge_length
|
|
29
|
+
from first 2 file. 'all' for exracting and checking these metadata
|
|
30
|
+
from all file.
|
|
31
|
+
:param timeinfo_format: str or (slice, str). Format for extracting start
|
|
32
|
+
time from file name.
|
|
33
|
+
:param timeinfo_from_basename: bool. If True, timeinfo_format will use
|
|
34
|
+
DASDateTime.strptime to basename of fpath.
|
|
35
|
+
:param nch: int. Channel number.
|
|
36
|
+
:param nt: int. Sampling points of each file.
|
|
37
|
+
:param dx: number. Channel interval in m.
|
|
38
|
+
:param fs: number. Sampling rate in Hz.
|
|
39
|
+
:param gauge_length: number. Gauge length in m.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(fpath, (list, tuple)):
|
|
42
|
+
self.flist = []
|
|
43
|
+
for fp in fpath:
|
|
44
|
+
self.flist.extend(glob(fp))
|
|
45
|
+
else:
|
|
46
|
+
self.flist = glob(fpath)
|
|
47
|
+
if not len(self.flist):
|
|
48
|
+
raise ValueError('No file input.')
|
|
49
|
+
self.flist.sort()
|
|
50
|
+
self.ftype = ftype
|
|
51
|
+
for key in ['nch', 'nt', 'dx', 'fs', 'gauge_length']:
|
|
52
|
+
if key in kwargs.keys():
|
|
53
|
+
setattr(self, key, kwargs[key])
|
|
54
|
+
if timeinfo_format is None and not meta_from_file:
|
|
55
|
+
meta_from_file = True
|
|
56
|
+
|
|
57
|
+
if meta_from_file == 'all':
|
|
58
|
+
ftime = []
|
|
59
|
+
metadata_list = []
|
|
60
|
+
for f in self.flist:
|
|
61
|
+
sec = read(f, ftype=ftype, headonly=True)
|
|
62
|
+
if not hasattr(sec, 'gauge_length'):
|
|
63
|
+
sec.gauge_length = None
|
|
64
|
+
ftime.append(sec.start_time)
|
|
65
|
+
metadata_list.append((sec.nch, sec.nt, sec.dx, sec.fs,
|
|
66
|
+
sec.gauge_length))
|
|
67
|
+
|
|
68
|
+
if len(set(metadata_list)) > 1:
|
|
69
|
+
warnings.warn('More than one kind of setting detected.')
|
|
70
|
+
metadata = max(metadata_list, key=metadata_list.count)
|
|
71
|
+
for i, key in enumerate(['nch', 'nt', 'dx', 'fs', 'gauge_length']):
|
|
72
|
+
if not hasattr(self, key):
|
|
73
|
+
setattr(self, key, metadata[i])
|
|
74
|
+
self.ftime = ftime
|
|
75
|
+
elif meta_from_file:
|
|
76
|
+
i = int(len(self.flist) > 1)
|
|
77
|
+
sec = read(self.flist[i], ftype=ftype, headonly=True)
|
|
78
|
+
if timeinfo_format is None:
|
|
79
|
+
if flength is None:
|
|
80
|
+
flength = sec.duration
|
|
81
|
+
self.ftime = [sec.start_time + (j - i) * flength for j in
|
|
82
|
+
range(len(self))]
|
|
83
|
+
if not hasattr(sec, 'gauge_length'):
|
|
84
|
+
sec.gauge_length = None
|
|
85
|
+
metadata = (sec.nch, sec.nt, sec.dx, sec.fs, sec.gauge_length)
|
|
86
|
+
for i, key in enumerate(['nch', 'nt', 'dx', 'fs', 'gauge_length']):
|
|
87
|
+
if not hasattr(self, key):
|
|
88
|
+
setattr(self, key, metadata[i])
|
|
89
|
+
|
|
90
|
+
if not hasattr(self, 'ftime'):
|
|
91
|
+
if isinstance(timeinfo_format, tuple):
|
|
92
|
+
timeinfo_slice, timeinfo_format = timeinfo_format
|
|
93
|
+
else:
|
|
94
|
+
timeinfo_slice = slice(None)
|
|
95
|
+
if timeinfo_from_basename:
|
|
96
|
+
self.ftime = [DASDateTime.strptime(
|
|
97
|
+
os.path.basename(f)[timeinfo_slice], timeinfo_format)
|
|
98
|
+
for f in self.flist]
|
|
99
|
+
else:
|
|
100
|
+
self.ftime = [DASDateTime.strptime(f[timeinfo_slice],
|
|
101
|
+
timeinfo_format) for f in self.flist]
|
|
102
|
+
|
|
103
|
+
self._sort()
|
|
104
|
+
if flength is None:
|
|
105
|
+
if len(self.flist) > 2:
|
|
106
|
+
time_diff = np.round(np.diff(self.ftime[1:]).astype(float))
|
|
107
|
+
flength_set, counts = np.unique(time_diff, return_counts=True)
|
|
108
|
+
if len(flength_set) > 1:
|
|
109
|
+
warnings.warn('File start times are unevenly spaced. Data '
|
|
110
|
+
'may not be continuous and self.flength may '
|
|
111
|
+
'be incorrectly detected.')
|
|
112
|
+
flength = flength_set[counts.argmax()]
|
|
113
|
+
elif len(self.flist) == 2:
|
|
114
|
+
flength = self.ftime[1] - self.ftime[0]
|
|
115
|
+
else:
|
|
116
|
+
flength = read(self.flist[0], ftype=ftype,
|
|
117
|
+
headonly=True).duration
|
|
118
|
+
elif flength <= 0:
|
|
119
|
+
raise ValueError('dt must > 0')
|
|
120
|
+
|
|
121
|
+
self.flength = flength
|
|
122
|
+
|
|
123
|
+
def __str__(self):
|
|
124
|
+
if len(self) == 1:
|
|
125
|
+
describe = f' flist: {self.flist}\n'
|
|
126
|
+
elif len(self) <= 5:
|
|
127
|
+
describe = f' flist: {len(self)} files\n' + \
|
|
128
|
+
f' {self.flist}\n'
|
|
129
|
+
else:
|
|
130
|
+
describe = f' flist: {len(self)} files\n' + \
|
|
131
|
+
f' [{self[0]},\n' + \
|
|
132
|
+
f' {self[1]},\n' + \
|
|
133
|
+
f' ...,\n' + \
|
|
134
|
+
f' {self[-1]}]\n'
|
|
135
|
+
|
|
136
|
+
describe += f' ftime: {self.start_time} to {self.end_time}\n' + \
|
|
137
|
+
f' flength: {self.flength}\n' + \
|
|
138
|
+
f' nch: {self.nch}\n' + \
|
|
139
|
+
f' nt: {self.nt}\n' + \
|
|
140
|
+
f' dx: {self.dx}\n' + \
|
|
141
|
+
f' fs: {self.fs}\n' + \
|
|
142
|
+
f'gauge_length: {self.gauge_length}\n'
|
|
143
|
+
|
|
144
|
+
return describe
|
|
145
|
+
|
|
146
|
+
__repr__ = __str__
|
|
147
|
+
|
|
148
|
+
def __getitem__(self, i):
|
|
149
|
+
return self.flist[i]
|
|
150
|
+
|
|
151
|
+
def __len__(self):
|
|
152
|
+
return len(self.flist)
|
|
153
|
+
|
|
154
|
+
def _sort(self):
|
|
155
|
+
sort = np.argsort(self.ftime)
|
|
156
|
+
self.ftime = [self.ftime[i] for i in sort]
|
|
157
|
+
self.flist = [self.flist[i] for i in sort]
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def start_time(self):
|
|
162
|
+
return self.ftime[0]
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def end_time(self):
|
|
166
|
+
return self.ftime[-1] + self.flength
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def duration(self):
|
|
170
|
+
return self.end_time - self.start_time
|
|
171
|
+
|
|
172
|
+
def select(self, stime=None, etime=None, readsec=False, **kwargs):
|
|
173
|
+
"""
|
|
174
|
+
Select a period of data.
|
|
175
|
+
|
|
176
|
+
:param stime, etime: DASDateTime. Start and end time of required data.
|
|
177
|
+
:param readsec: bool. If True, read as a instance of daspy.Section and
|
|
178
|
+
return. If False, update self.flist.
|
|
179
|
+
:param ch1: int. The first channel required. Only works when
|
|
180
|
+
readsec=True.
|
|
181
|
+
:param ch2: int. The last channel required (not included). Only works
|
|
182
|
+
when readsec=True.
|
|
183
|
+
"""
|
|
184
|
+
if stime is None:
|
|
185
|
+
stime = self.ftime[0]
|
|
186
|
+
elif stime - self.ftime[0] < 0:
|
|
187
|
+
warnings.warn('stime is earlier than the start time of the first '
|
|
188
|
+
'file. Set stime to self.ftime[0].')
|
|
189
|
+
|
|
190
|
+
if etime is None:
|
|
191
|
+
etime = self.ftime[-1] + self.flength
|
|
192
|
+
elif etime - self.ftime[-1] > self.flength:
|
|
193
|
+
warnings.warn('etime is later than the end time of the last file. '
|
|
194
|
+
'Set etime to self.ftime[-1] + self.flength.')
|
|
195
|
+
|
|
196
|
+
if stime > etime:
|
|
197
|
+
raise ValueError('Start time can\'t be later than end time.')
|
|
198
|
+
|
|
199
|
+
flist = []
|
|
200
|
+
ftime = []
|
|
201
|
+
for i in range(len(self)):
|
|
202
|
+
if (stime - self.flength) < self.ftime[i] < etime:
|
|
203
|
+
flist.append(self.flist[i])
|
|
204
|
+
ftime.append(self.ftime[i])
|
|
205
|
+
|
|
206
|
+
if readsec:
|
|
207
|
+
sec = read(flist[0], **kwargs)
|
|
208
|
+
for f in flist[1:]:
|
|
209
|
+
sec += read(f, **kwargs)
|
|
210
|
+
sec.trimming(tmin=stime, tmax=etime)
|
|
211
|
+
return sec
|
|
212
|
+
else:
|
|
213
|
+
self.flist = flist
|
|
214
|
+
self.ftime = ftime
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def _optimize_for_continuity(self, operations):
|
|
218
|
+
method_list = []
|
|
219
|
+
kwargs_list = []
|
|
220
|
+
if not isinstance(operations[0], (list, tuple)):
|
|
221
|
+
operations = [operations]
|
|
222
|
+
for opera in operations:
|
|
223
|
+
method, kwargs = opera
|
|
224
|
+
if method == 'downsampling':
|
|
225
|
+
if hasattr(kwargs, 'lowpass_filter') and not\
|
|
226
|
+
kwargs['lowpass_filter']:
|
|
227
|
+
method_list.append('downsampling')
|
|
228
|
+
kwargs_list.append(kwargs)
|
|
229
|
+
else:
|
|
230
|
+
method_list.extend(['lowpass_cheby_2', 'downsampling'])
|
|
231
|
+
kwargs['lowpass_filter'] = False
|
|
232
|
+
kwargs0 = dict(freq=self.fs/2/kwargs['tint'], zi=0)
|
|
233
|
+
kwargs_list.extend([kwargs0, kwargs])
|
|
234
|
+
else:
|
|
235
|
+
if method in cascade_method:
|
|
236
|
+
kwargs.setdefault('zi', 0)
|
|
237
|
+
|
|
238
|
+
method_list.append(method)
|
|
239
|
+
kwargs_list.append(kwargs)
|
|
240
|
+
return method_list, kwargs_list
|
|
241
|
+
|
|
242
|
+
def process(self, operations, savepath='./processed',
|
|
243
|
+
suffix='_pro', ftype=None, **read_kwargs):
|
|
244
|
+
"""
|
|
245
|
+
:param savepath:
|
|
246
|
+
:param ch1: int. The first channel required.
|
|
247
|
+
:param ch2: int. The last channel required (not included).
|
|
248
|
+
"""
|
|
249
|
+
if not os.path.exists(savepath):
|
|
250
|
+
os.makedirs(savepath)
|
|
251
|
+
method_list, kwargs_list = self._optimize_for_continuity(operations)
|
|
252
|
+
new_flist = []
|
|
253
|
+
for i in tqdm(range(len(self))):
|
|
254
|
+
f = self[i]
|
|
255
|
+
sec = read(f, ftype=self.ftype, **read_kwargs)
|
|
256
|
+
for j, method in enumerate(method_list):
|
|
257
|
+
if method == 'taper':
|
|
258
|
+
if i == 0:
|
|
259
|
+
win = cosine_taper(np.ones_like(sec.data), **kwargs_list[j])
|
|
260
|
+
win[:, -sec.nt//2:] = 1
|
|
261
|
+
sec.data *= win
|
|
262
|
+
elif i == len(self) - 1:
|
|
263
|
+
win = cosine_taper(np.ones_like(sec.data), **kwargs_list[j])
|
|
264
|
+
win[:, :sec.nt//2] = 1
|
|
265
|
+
sec.data *= win
|
|
266
|
+
else:
|
|
267
|
+
out = eval(f'sec.{method}')(**kwargs_list[j])
|
|
268
|
+
if method == 'time_integration':
|
|
269
|
+
kwargs_list[j]['c'] = sec.data[:, -1]
|
|
270
|
+
elif method == 'time_differential':
|
|
271
|
+
kwargs_list[j]['prepend'] = sec.data[:, -1]
|
|
272
|
+
elif method in cascade_method:
|
|
273
|
+
kwargs_list[j]['zi'] = out
|
|
274
|
+
f0, f1 = os.path.splitext(os.path.basename(f))
|
|
275
|
+
if ftype is not None:
|
|
276
|
+
f1 = ftype
|
|
277
|
+
filepath = os.path.join(savepath, f0+suffix+f1)
|
|
278
|
+
sec.save(filepath)
|
|
279
|
+
new_flist.append(filepath)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Purpose: Module for handling DASDateTime objects.
|
|
2
|
+
# Author: Minzhe Hu
|
|
3
|
+
# Date: 2024.9.25
|
|
4
|
+
# Email: hmz2018@mail.ustc.edu.cn
|
|
5
|
+
import time
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
utc = timezone.utc
|
|
11
|
+
local_tz = timezone(timedelta(seconds=-time.altzone))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DASDateTime(datetime):
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def __add__(self, other):
|
|
18
|
+
if isinstance(other, Iterable):
|
|
19
|
+
out = []
|
|
20
|
+
for t in other:
|
|
21
|
+
out.append(self + t)
|
|
22
|
+
return out
|
|
23
|
+
elif not isinstance(other, timedelta):
|
|
24
|
+
other = timedelta(seconds=float(other))
|
|
25
|
+
return super().__add__(other)
|
|
26
|
+
|
|
27
|
+
def __sub__(self, other):
|
|
28
|
+
if isinstance(other, Iterable):
|
|
29
|
+
out = []
|
|
30
|
+
for t in other:
|
|
31
|
+
out.append(self - t)
|
|
32
|
+
return out
|
|
33
|
+
elif isinstance(other, datetime):
|
|
34
|
+
return datetime.__sub__(*self._unify_tz(other)).total_seconds()
|
|
35
|
+
elif not isinstance(other, timedelta):
|
|
36
|
+
other = timedelta(seconds=other)
|
|
37
|
+
return super().__sub__(other)
|
|
38
|
+
|
|
39
|
+
def __le__(self, other):
|
|
40
|
+
return datetime.__le__(*self._unify_tz(other))
|
|
41
|
+
|
|
42
|
+
def __lt__(self, other):
|
|
43
|
+
return datetime.__lt__(*self._unify_tz(other))
|
|
44
|
+
|
|
45
|
+
def __ge__(self, other):
|
|
46
|
+
return datetime.__ge__(*self._unify_tz(other))
|
|
47
|
+
|
|
48
|
+
def __gt__(self, other):
|
|
49
|
+
return datetime.__gt__(*self._unify_tz(other))
|
|
50
|
+
|
|
51
|
+
def _unify_tz(self, other):
|
|
52
|
+
if self.tzinfo and not other.tzinfo:
|
|
53
|
+
return self, other.replace(tzinfo=self.tzinfo)
|
|
54
|
+
elif not self.tzinfo and other.tzinfo:
|
|
55
|
+
return self.replace(tzinfo=other.tzinfo), other
|
|
56
|
+
return self, other
|
|
57
|
+
|
|
58
|
+
def local(self):
|
|
59
|
+
return self.astimezone(tz=local_tz)
|
|
60
|
+
|
|
61
|
+
def utc(self):
|
|
62
|
+
return self.astimezone(tz=utc)
|
|
63
|
+
|
|
64
|
+
def remove_tz(self):
|
|
65
|
+
return self.replace(tzinfo=None)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_datetime(cls, dt: datetime):
|
|
69
|
+
return cls.fromtimestamp(dt.timestamp(), tz=dt.tzinfo)
|
|
70
|
+
|
|
71
|
+
def to_datetime(self):
|
|
72
|
+
return datetime.fromtimestamp(self.timestamp(), tz=self.tzinfo)
|
daspy/core/example.pkl
ADDED
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import h5py
|
|
2
|
+
import numpy as np
|
|
3
|
+
from daspy import read, Section, DASDateTime
|
|
4
|
+
from daspy.core.dasdatetime import utc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
origin_time = DASDateTime(2016, 3, 21, 7, 37, 10, 535000, tzinfo=utc)
|
|
8
|
+
|
|
9
|
+
# read DAS data
|
|
10
|
+
dx, fs = 1, 1000
|
|
11
|
+
sec = Section(np.zeros((8721, 0)), dx, fs, data_type='Strain rate')
|
|
12
|
+
filename = ['PoroTomo_iDAS16043_160321073651.h5',
|
|
13
|
+
'PoroTomo_iDAS16043_160321073721.h5',
|
|
14
|
+
'PoroTomo_iDAS16043_160321073751.h5',
|
|
15
|
+
'PoroTomo_iDAS16043_160321073821.h5']
|
|
16
|
+
|
|
17
|
+
first = True
|
|
18
|
+
for fn in filename:
|
|
19
|
+
with h5py.File(fn,'r') as fp:
|
|
20
|
+
if first:
|
|
21
|
+
sec.start_time = DASDateTime.fromtimestamp(fp['t'][0]).astimezone(utc)
|
|
22
|
+
sec.start_channel = fp['channel'][0]
|
|
23
|
+
first = False
|
|
24
|
+
sec += fp['das'][()].T
|
|
25
|
+
|
|
26
|
+
sec.origin_time = origin_time
|
|
27
|
+
sec.trimming(mode=0, xmin=2500, xmax=3000)
|
|
28
|
+
sec.trimming(tmin=origin_time, tmax=origin_time+90)
|
|
29
|
+
sec.downsampling(tint=10)
|
|
30
|
+
sec.trimming(tmin=origin_time+20, tmax=origin_time+70)
|
|
31
|
+
|
|
32
|
+
sec.save('example.py')
|