asmu 0.0.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.

Potentially problematic release.


This version of asmu might be problematic. Click here for more details.

asmu/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .analysis import get_corrs_sampleshifts, calSPL
2
+ from .generator import SincBurst, Sine, WhiteNoise, SineBurst, Chirp
3
+ from .inout import Input, Output
4
+ from .interface import Interface
5
+ from .recording import Recording
6
+ from .setup import Setup
7
+ #NOT IMPLEMENTED from .view import TimeView, Corrview
8
+
9
+ __all__ = ["get_corrs_sampleshifts", "calSPL", "SincBurst", "Sine", "WhiteNoise", "SineBurst", "Chirp", "generator", "Input", "Output", "Interface", "Recording", "Setup"]
asmu/analysis.py ADDED
@@ -0,0 +1,68 @@
1
+ import numpy as np
2
+ import scipy as sp
3
+ import queue
4
+
5
+ class Analyzer():
6
+ def __init__(self, chunk, queuesize):
7
+ self._chunk = chunk
8
+
9
+ self._w = np.hanning(chunk)
10
+ self._q = queue.Queue(queuesize)
11
+
12
+ def reset(self):
13
+ # process full queue
14
+ while not self._q.empty():
15
+ self.process_queue()
16
+
17
+ def put_queue(self, data):
18
+ self._q.put(data.copy())
19
+
20
+ def process_queue(self):
21
+ pass
22
+
23
+ def _get_fft(self) -> np.ndarray:
24
+ return np.fft.rfft(self._q.get()*self._w, axis=0, norm="forward")*2/np.mean(self._w)
25
+
26
+ class calSPL(Analyzer):
27
+ def __init__(self, chunk, queuesize: int=10):
28
+ self._uks = []
29
+ self._ks = []
30
+ super().__init__(chunk, queuesize)
31
+
32
+ def process_queue(self):
33
+ ukm = self._get_fft()
34
+
35
+ kmax = np.argmax(np.abs(ukm))
36
+ self._uks.append(np.abs(ukm[kmax]))
37
+ self._ks.append(kmax)
38
+
39
+ def get_cSPL(self, spl):
40
+ self.reset()
41
+
42
+ p0 = 2e-5 # [Pa]Rms
43
+ p = p0 * 10 ** (spl / 20) # [Pa]Rms
44
+ pcal = p * np.sqrt(2) # [Pa]
45
+
46
+ return pcal/np.mean(self._uks[2:]) # dont use the first three measurements
47
+
48
+ def get_kSPL(self):
49
+ return round(np.mean(self._ks))
50
+
51
+ def get_corrs_sampleshifts(indata: np.ndarray, refdata: np.ndarray, burstLength: int) -> tuple[np.ndarray, np.ndarray]:
52
+ corrs = []
53
+ sampleshifts = []
54
+ n = int(np.size(refdata))
55
+ for cidx in range(np.ma.size(indata, axis=1)):
56
+ v = indata[:, cidx]
57
+ corr = sp.signal.correlate(v, refdata)
58
+ corr /= np.max(corr)
59
+ peaks, _ = sp.signal.find_peaks(corr, prominence=0.8, distance=burstLength, height=0.5)
60
+
61
+ # shift correlation correctly
62
+ corrs.append(corr[n-1:])
63
+ sampleshifts.append(peaks[0]-(n-1))
64
+ return np.array(corrs).T, np.array(sampleshifts)
65
+
66
+
67
+
68
+
asmu/generator.py ADDED
@@ -0,0 +1,158 @@
1
+ import numpy as np
2
+ import scipy as sp
3
+ import threading
4
+ import queue
5
+
6
+ class Generator():
7
+ def __init__(self, queuesize) -> None:
8
+ self._q = queue.Queue(queuesize)
9
+ self._prefill_queue()
10
+
11
+ def get_queue(self) -> queue.Queue:
12
+ return self._q.get_nowait()
13
+
14
+ def _prefill_queue(self) -> None:
15
+ while self._q.full() is False:
16
+ self.refill_queue(block=False)
17
+
18
+ def refill_queue(self, block=True) -> None:
19
+ #if self._q.full() is False:
20
+ data = self._gen()
21
+ self._q.put(data, block=block, timeout=0.2)
22
+
23
+ def start_refill_thread(self, stream):
24
+ def runner(self, stream):
25
+ while stream.active:
26
+ try:
27
+ self.refill_queue()
28
+ except queue.Full:
29
+ pass
30
+ threading.Thread(target=runner, args=(self, stream, )).start()
31
+
32
+ def _gen(self) -> None:
33
+ return None
34
+
35
+ class Chirp(Generator):
36
+ def __init__(self, rate: int, chunk: int, f0: float, f1: float, duration: int, queuesize: int=10) -> None:
37
+ self._rate = rate
38
+ self._chunk = chunk
39
+
40
+ t = np.arange(0, int(duration*rate)) / rate
41
+ self._chirp = sp.signal.chirp(t, f0=f0, t1=duration, f1=f1, method="logarithmic")
42
+ self._idx = 0
43
+ super().__init__(queuesize)
44
+
45
+ def _gen(self) -> np.ndarray:
46
+ data = self._chirp[self._idx:self._idx+self._chunk]
47
+ self._idx += self._chunk
48
+
49
+ length = np.ma.size(data, axis=0)
50
+ if data.size == 0:
51
+ return None
52
+ elif length < self._chunk:
53
+ pad = np.zeros(self._chunk)
54
+ pad[:length] = data
55
+ return pad
56
+ else:
57
+ return data
58
+
59
+
60
+ class SineBurst(Generator):
61
+ def __init__(self, rate: int, chunk: int, f: float, n: int, loop: float=-1, queuesize: int=10) -> None:
62
+ self._rate = rate
63
+ self._chunk = chunk
64
+ self._freq = f
65
+
66
+ # generate wave parameters
67
+ self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
68
+ self._phi = 0
69
+ self._maxang = 2*np.pi*n
70
+ self._loopang = 2*np.pi*loop*f
71
+
72
+ super().__init__(queuesize)
73
+
74
+ @property
75
+ def burstlen(self):
76
+ return self._rate/(2*np.pi*self._freq)*self._maxang
77
+
78
+ def _gen(self) -> np.ndarray:
79
+ ang = self._omegas + self._phi
80
+ if self._loopang > 0: ang = np.mod(ang, self._loopang)
81
+ sineburst = np.sin(ang)
82
+ sineburst[ang > self._maxang] = 0
83
+ self._phi += 2*np.pi*self._freq*self._chunk/self._rate
84
+ return sineburst
85
+
86
+ class SincBurst(Generator):
87
+ def __init__(self, rate: int, chunk: int, f: float, n: int, loop: float=-1, queuesize: int=10) -> None:
88
+ self._rate = rate
89
+ self._chunk = chunk
90
+ self._freq = f
91
+
92
+ # generate wave parameters
93
+ self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
94
+ self._phi = 0
95
+ self._maxang = 2*np.pi*n
96
+ self._loopang = 2*np.pi*loop*f
97
+
98
+ super().__init__(queuesize)
99
+
100
+ @property
101
+ def burstlen(self):
102
+ return self._rate/(2*np.pi*self._freq)*self._maxang
103
+
104
+ def _gen(self) -> np.ndarray:
105
+ ang = self._omegas + self._phi
106
+ if self._loopang > 0: ang = np.mod(ang, self._loopang)
107
+ sincburst = np.sinc((ang-self._maxang/2)/np.pi)
108
+ sincburst[ang > self._maxang] = 0
109
+ self._phi += 2*np.pi*self._freq*self._chunk/self._rate
110
+ return sincburst
111
+
112
+ class WhiteNoise(Generator):
113
+ def __init__(self, chunk: int, queuesize: int=10) -> None:
114
+ self._chunk = chunk
115
+
116
+ super().__init__(queuesize)
117
+
118
+ def _gen(self) -> np.ndarray:
119
+ fwhite = np.fft.rfft(np.random.randn(self._chunk))
120
+ return np.clip(np.fft.irfft(fwhite)/4, -1, 1)
121
+
122
+ class PinkNoise(Generator):
123
+ def __init__(self, chunk: int, queuesize: int=10) -> None:
124
+ self._chunk = chunk
125
+
126
+ super().__init__(queuesize)
127
+
128
+ def _gen(self) -> np.ndarray:
129
+ fwhite = np.fft.rfft(np.random.randn(self._chunk))
130
+
131
+ # apply noise color and normalize for power
132
+ f = np.fft.rfftfreq(self._chunk)
133
+ S = 1/np.where(f == 0, float('inf'), np.sqrt(f))
134
+ S = S / np.sqrt(np.mean(S**2))
135
+ fpink = fwhite * S
136
+
137
+ return np.clip(np.fft.irfft(fpink)/4, -1, 1)
138
+
139
+ class Sine(Generator):
140
+ def __init__(self, rate: int, chunk: int, f: float, queuesize: int=10) -> None:
141
+ self._rate = rate
142
+ self._chunk = chunk
143
+ self._freq = f
144
+
145
+ # generate wave parameters
146
+ self._omegas = np.linspace(0, 2*np.pi*f*chunk/rate, chunk, endpoint=False, dtype=np.float32)
147
+ self._phi = 0
148
+
149
+ super().__init__(queuesize)
150
+
151
+ def _gen(self) -> np.ndarray:
152
+ ang = self._omegas + self._phi
153
+ self._phi += 2*np.pi*self._freq*self._chunk/self._rate
154
+ self._phi %= 2*np.pi
155
+ return np.sin(ang)
156
+
157
+
158
+
asmu/inout.py ADDED
@@ -0,0 +1,79 @@
1
+ import numpy as np
2
+
3
+ class IO():
4
+ def __init__(self, IOsetup: dict) -> None:
5
+ self._IOSetup = IOsetup
6
+
7
+ def __repr__(self):
8
+ return self.name
9
+
10
+ @property
11
+ def name(self):
12
+ return self._IOSetup["name"]
13
+
14
+ @property
15
+ def channel(self):
16
+ return int(self._IOSetup["channel"])
17
+
18
+ @property
19
+ def color(self):
20
+ return self._IOSetup["color"]
21
+ @color.setter
22
+ def color(self, value):
23
+ self._IOSetup["color"] = str(value)
24
+
25
+ @property
26
+ def pos(self):
27
+ return np.array(self._IOSetup["pos"])
28
+ @pos.setter
29
+ def pos(self, value):
30
+ self._IOSetup["pos"] = value
31
+
32
+ @property
33
+ def reference(self):
34
+ try:
35
+ return bool(self._IOSetup["reference"])
36
+ except KeyError:
37
+ return False
38
+ @reference.setter
39
+ def reference(self, value):
40
+ if value:
41
+ self._IOSetup["reference"] = True
42
+ else:
43
+ try:
44
+ del self._IOSetup["reference"]
45
+ except KeyError:
46
+ pass
47
+
48
+ class Input(IO):
49
+ def __init__(self, inputSetup: dict) -> None:
50
+ super().__init__(inputSetup)
51
+ self._inputSetup = inputSetup
52
+
53
+ @property
54
+ def cSPL(self):
55
+ return float(self._inputSetup["cSPL"])
56
+ @cSPL.setter
57
+ def cSPL(self, value:float):
58
+ self._inputSetup["cSPL"] = value
59
+
60
+ @property
61
+ def kSPL(self):
62
+ return int(self._inputSetup["kSPL"])
63
+ @kSPL.setter
64
+ def kSPL(self, value:int):
65
+ self._inputSetup["kSPL"] = value
66
+
67
+ @property
68
+ def cFR(self):
69
+ return self._inputSetup["cFR"]
70
+ @cFR.setter
71
+ def cFR(self, value):
72
+ self._inputSetup["cFR"] = np.array(value)
73
+
74
+
75
+ class Output(IO):
76
+ def __init__(self, outputSetup: dict) -> None:
77
+ super().__init__(outputSetup)
78
+
79
+
asmu/interface.py ADDED
@@ -0,0 +1,107 @@
1
+ import numpy as np
2
+ import sounddevice as sd
3
+
4
+ DEBUG = True
5
+
6
+ class Interface():
7
+ def __init__(self, interfaceSetup: dict) -> None:
8
+ """Class for Audiointerface communication and settings.
9
+
10
+ Args:
11
+ interfaceSetup (dict): Interface setup.
12
+ """
13
+ self._interfaceSetup = interfaceSetup
14
+ self.callback = None
15
+
16
+ @property
17
+ def name(self):
18
+ return self._interfaceSetup["name"]
19
+
20
+ @property
21
+ def device(self):
22
+ return self._interfaceSetup["device"]
23
+ @device.setter
24
+ def device(self, value):
25
+ self._interfaceSetup["device"] = value
26
+
27
+ @property
28
+ def rate(self):
29
+ return int(self._interfaceSetup["rate"])
30
+
31
+ @property
32
+ def chunk(self):
33
+ return int(self._interfaceSetup['chunk'])
34
+
35
+ # SOUNDDEVICE
36
+ def initSounddevice(self, channels: tuple[int, int]) -> None:
37
+ sd.default.samplerate = self.rate
38
+ sd.default.dtype = np.float32
39
+ sd.default.blocksize = self.chunk
40
+ try:
41
+ sd.default.device = self.device # TODO - different input and output device
42
+ except KeyError:
43
+ print("interface - no device specified, using default")
44
+ return
45
+
46
+ if "ASIO" in self.device:
47
+ if channels[0]:
48
+ inChannels = [c - 1 for c in channels[0]] # convert to channel names starting with 0
49
+ asio_in = sd.AsioSettings(channel_selectors=inChannels)
50
+
51
+ if not channels[1]:
52
+ sd.default.extra_settings = asio_in
53
+ sd.default.channels = len(inChannels)
54
+ return
55
+
56
+ if channels[1]:
57
+ outChannels = [c - 1 for c in channels[1]]
58
+ asio_out = sd.AsioSettings(channel_selectors=outChannels)
59
+
60
+ if not channels[0]:
61
+ sd.default.extra_settings = asio_out
62
+ sd.default.channels = len(outChannels)
63
+ return
64
+
65
+ if channels[0] and channels[1]:
66
+ sd.default.extra_settings = (asio_in, asio_out)
67
+ sd.default.channels = (len(inChannels), len(outChannels))
68
+ return
69
+
70
+ elif "CoreAudio" in self.device: # TODO - update this
71
+ inChannels = [c - 1 for c in channels[0]] # convert to channel names starting with 0
72
+ ca_in = sd.CoreAudioSettings(channel_map=inChannels)
73
+
74
+ if channels[1]:
75
+ outChannels = [-1]*sd.query_devices(device=self.device, kind="output")["max_output_channels"]
76
+ for idx, c in enumerate(channels[1]):
77
+ outChannels[c-1] = idx
78
+ ca_out = sd.CoreAudioSettings(channel_map=outChannels)
79
+
80
+ sd.default.extra_settings = (ca_in, ca_out)
81
+ sd.default.channels = (len(inChannels), len(outChannels))
82
+ else:
83
+ sd.default.extra_settings = ca_in
84
+ sd.default.channels = len(inChannels)
85
+ else:
86
+ raise NotImplementedError
87
+
88
+ def _callback(self, indata: np.ndarray, outdata: np.ndarray, frames: int, time, status) -> None:
89
+ if status:
90
+ print(status, flush=True)
91
+
92
+ outdata.fill(0)
93
+ self.callback(indata, outdata, frames, time)
94
+
95
+ def start_stream(self, channels:tuple, finished_callback=None) -> None:
96
+ self.initSounddevice(channels)
97
+ return sd.Stream(callback=self._callback, finished_callback=finished_callback)
98
+
99
+ def stop_stream(self) -> None:
100
+ raise sd.CallbackStop
101
+
102
+
103
+
104
+
105
+
106
+
107
+
asmu/recording.py ADDED
@@ -0,0 +1,195 @@
1
+ import json
2
+ import numpy as np
3
+ import queue
4
+ import soundfile as sf
5
+ import tempfile
6
+ import shutil
7
+ import os
8
+ import threading
9
+
10
+ from datetime import datetime
11
+
12
+ class Recording():
13
+ def __init__(self, recordingPath: str, mode: str, setupName: str, channels: int, rate: int, chunk: int, queuesize=10) -> None:
14
+ """Constructs all the necessary attributes for the Recording object.
15
+
16
+ Args:
17
+ recordingPath (str): Path to an .asm_rec file
18
+ mode ("r", "w"): Set recording mode to read "r", or write "w"
19
+ setupName (str): Name of the current setup for comparison with the recordings setupName
20
+ channels (int): Channel count
21
+ rate (int): Samplerate
22
+ chunk (int): Chunksize
23
+ queuesize (int, optional): The size of the internal queue. Defaults to 10.
24
+ """
25
+ # path and recording info
26
+ self._path, file = os.path.split(recordingPath)
27
+ self._name = file.replace(".asm_recording", "") # extract name without file ending
28
+ now = datetime.now()
29
+ self._recordingData = {
30
+ "setupName" : setupName,
31
+ "date" : now.strftime("%d/%m/%Y"),
32
+ "time" : now.strftime("%H:%M:%S"),
33
+ }
34
+
35
+ # save parameters
36
+ self._mode = mode
37
+ self._rate = rate
38
+ self._chunk = chunk
39
+ self._channels = channels
40
+
41
+ # create buffer queue for transfer between threads
42
+ self._rq = queue.Queue(queuesize)
43
+ self._pq = queue.Queue(queuesize)
44
+
45
+ def __enter__(self):
46
+ if self._mode=="r":
47
+ self.load_file(self.setupName)
48
+ elif self._mode=="w":
49
+ self.create_file()
50
+ else:
51
+ raise Exception(f"Recording - invalid mode specified, use \"r\" or \"w\".")
52
+
53
+ return self
54
+
55
+ def __exit__(self, exc_type, exc_value, exc_traceback):
56
+ self._filesf.close()
57
+
58
+ @property
59
+ def name(self):
60
+ return self._name
61
+
62
+ @property
63
+ def path(self):
64
+ return self._path
65
+
66
+ @property
67
+ def setupName(self):
68
+ return self._recordingData["setupName"]
69
+
70
+ @property
71
+ def date(self):
72
+ return self._recordingData["date"]
73
+
74
+ @property
75
+ def time(self):
76
+ return self._recordingData["time"]
77
+
78
+ @property
79
+ def extra_settings(self):
80
+ """:obj:`dict` of :obj:`dict`: Store extra settings in .asm_rec file.
81
+
82
+ Setting the dict overwrites existing extra_settings!
83
+ """
84
+ return self._recordingData["extra_settings"]
85
+ @extra_settings.setter
86
+ def extra_settings(self, value):
87
+ self._recordingData["extra_settings"] = value
88
+
89
+ @property
90
+ def latency(self):
91
+ try:
92
+ return self._recordingData["latency"]
93
+ except KeyError:
94
+ return None
95
+ @latency.setter
96
+ def latency(self, value):
97
+ self._recordingData["latency"] = int(value)
98
+
99
+ @property
100
+ def data(self):
101
+ # process full queue
102
+ while not self._rq.empty():
103
+ self.process_queue()
104
+
105
+ self._filesf.flush()
106
+ self._filesf.seek(0)
107
+ return self._filesf.read(dtype="float32", always_2d=True)
108
+
109
+ # LATENCY
110
+ def set_latency(self, time):
111
+ self.latency = round((time.outputBufferDacTime-time.inputBufferAdcTime)*self._rate + 1) # the +1 was measured experimentally (could be the cable?)
112
+
113
+ # PLAYBACK
114
+ def get_queue(self):
115
+ return self._pq.get_nowait()
116
+
117
+ def prefill_queue(self):
118
+ self.empty_queue()
119
+ self._filesf.seek(0)
120
+ while self._pq.full() is False:
121
+ self.refill_queue(block=False)
122
+
123
+ def refill_queue(self, block=True):
124
+ if self._pq.full() is False:
125
+ data = self._gen()
126
+ self._pq.put(data, block=block)
127
+
128
+ def empty_queue(self):
129
+ while not self._pq.empty():
130
+ self.get_queue()
131
+
132
+ def _gen(self):
133
+ data = self._filesf.read(self._chunk, dtype="float32", always_2d=True)
134
+ length = np.ma.size(data, axis=0)
135
+ if data.size == 0:
136
+ return None
137
+ elif length < self._chunk:
138
+ pad = np.zeros((self._chunk, self._channels))
139
+ pad[:length, :] = data
140
+ return pad
141
+ else:
142
+ return data
143
+
144
+ # RECORDING
145
+ def create_file(self, subtype="PCM_24"):
146
+ self._file = tempfile.NamedTemporaryFile(prefix=f"{self.name}", suffix='.wav', dir="", delete=True)
147
+ self._filesf = sf.SoundFile(self._file, mode='w+', samplerate=self._rate, channels=self._channels, subtype=subtype, format="WAV")
148
+
149
+ def put_queue(self, data):
150
+ self._rq.put(data.copy())
151
+
152
+ def process_queue(self):
153
+ self._filesf.write(self._rq.get(timeout=0.2))
154
+
155
+ def start_process_thread(self, stream):
156
+ def runner(self, stream):
157
+ while stream.active:
158
+ try:
159
+ self.process_queue()
160
+ except queue.Empty:
161
+ pass
162
+ threading.Thread(target=runner, args=(self, stream, )).start()
163
+
164
+ # SAVE/LOAD
165
+ def save_file(self, extra_settings={}):
166
+ # add new extra_settings
167
+ if extra_settings:
168
+ self.extra_settings = extra_settings
169
+
170
+ # save .asm_rec file
171
+ with open(f"{self.path}/{self.name}.asm_rec", 'w') as file:
172
+ file.write(json.dumps(self._recordingData))
173
+
174
+ # process full queue
175
+ while not self._rq.empty():
176
+ self.process_queue()
177
+
178
+ # persist temporary files in specified recording location
179
+ self._file.flush()
180
+ self._file.seek(0)
181
+ with open(f"{self.path}/{self.name}.wav", 'wb') as file:
182
+ shutil.copyfileobj(self._file, file)
183
+
184
+ def load_file(self, currentSetupName):
185
+ # open .asm_rec file
186
+ with open(f"{self.path}/{self.name}.asm_rec", 'r') as file:
187
+ self._recordingData = json.load(file)
188
+
189
+ # check if setups are equal
190
+ if self.setupName != currentSetupName:
191
+ raise Exception(f"Recording - current setup does not match setup of {self.setupName}.asm_rec")
192
+
193
+ # open .wav file
194
+ self._filesf = sf.SoundFile(f"{self.path}/{self.name}.wav")
195
+ self.prefill_queue()
asmu/setup.py ADDED
@@ -0,0 +1,79 @@
1
+ import json
2
+ import numpy as np
3
+ import os
4
+
5
+ class Setup():
6
+ def __init__(self, setupPath: str) -> None:
7
+ """Class to handle setups.
8
+
9
+ Args:
10
+ setupPath (str): Path to .asm_setup file
11
+ """
12
+ self._path, file = os.path.split(setupPath)
13
+ self._name = file.replace(".asm_setup", "") # extract name without file ending
14
+ self.load_file(setupPath)
15
+
16
+ @property
17
+ def name(self):
18
+ return self._name
19
+
20
+ @property
21
+ def path(self):
22
+ return self._path
23
+
24
+ @property
25
+ def inputs(self):
26
+ try:
27
+ value = self._setup["inputs"]
28
+ return value
29
+ except KeyError:
30
+ return []
31
+
32
+ @property
33
+ def outputs(self):
34
+ try:
35
+ value = self._setup["outputs"]
36
+ return value
37
+ except KeyError:
38
+ return []
39
+
40
+ @property
41
+ def interface(self):
42
+ return self._setup["interface"]
43
+
44
+ def load_file(self, setupPath):
45
+ with open(setupPath, "r") as file:
46
+ self._setup = json.load(file)
47
+
48
+ # load numpy arrays from external files
49
+ for io in self.inputs+self.outputs:
50
+ try:
51
+ path = f"{self.path}/{self.name}/cFR_{io["name"]}.npy"
52
+ io["cFR"] = np.load(path)
53
+ except FileNotFoundError:
54
+ pass
55
+
56
+ def save_file(self, setupPath=None):
57
+ if setupPath is not None:
58
+ self.__init__(setupPath)
59
+ setupPath = f"{self.path}/{self.name}.asm_setup"
60
+
61
+ # save numpy arrays to external files
62
+ for io in self.inputs+self.outputs:
63
+ try:
64
+ if isinstance(io["cFR"], np.ndarray):
65
+ path = f"{self.path}/{self.name}/cFR_{io["name"]}.npy"
66
+ np.save(path, io["cFR"])
67
+ io.pop("cFR")
68
+ except KeyError:
69
+ pass
70
+
71
+ # save .asm_setup file
72
+ with open(setupPath, 'w') as file:
73
+ file.write(json.dumps(self._setup, sort_keys=True, indent=4, separators=(',', ': ')))
74
+
75
+
76
+
77
+
78
+
79
+